<?xml version="1.0" encoding="UTF-8" standalone="no"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:idx="urn:atom-extension:indexing" xmlns:media="http://search.yahoo.com/mrss/" idx:index="no"><subtitle>tipy na zajímavé články</subtitle>
    <!--
Content-type: Preventing XSRF in IE.

-->
    <generator uri="https://cloud.feedly.com">feedly cloud</generator>
    <id>tag:feedly.com,2013:cloud/feed/https://feedly.com/f/rJIIpYUxxvSQIe3s6CKpJaO8</id>
    <link href="https://feedly.com/f/rJIIpYUxxvSQIe3s6CKpJaO8" rel="self" type="application/rss+xml"/>
    <link href="https://feedly.com/f/rJIIpYUxxvSQIe3s6CKpJaO8?continuation=19c946c3ea2:31b24:fa3c592a" rel="next" type="application/rss+xml"/>
    <title>rarouš.w3b</title>
    <updated>2026-04-27T08:11:06Z</updated>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19dcdfde5b1:853577:18edc835</id>
        <title type="html">Crashing hard: why talking about bubbles obscures the real social cost of overinvesting into “Artificial Intelligence”</title>
        <published>2026-04-27T08:11:01Z</published>
        <updated>2026-04-27T08:11:06Z</updated>
        <link href="https://www.structural-integrity.eu/crashing-hard-why-talking-about-bubbles-obscures-the-real-social-cost-of-overinvesting-into-artificial-intelligence/" rel="alternate" type="text/html"/>
        <summary type="html">More and more commentators talk about and warn of an “AI bubble”, and everybody seems to congratulate each other on being such a smart financial analyst. BUT: A bubble pops and you are left with air and maybe a splash of soap somewhere on the floor. A fairly clean affair. This kind of investor speak</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
&lt;p&gt;&lt;a href="https://observer.co.uk/news/science-technology/article/a-new-dotcom-bubble-ai-hype-has-yet-to-translate-into-profits"&gt;More&lt;/a&gt; &lt;a href="https://fluxus.io/article/a-hitchhikers-guide-to-the-ai-bubble"&gt;and&lt;/a&gt; &lt;a href="https://www.goldmansachs.com/insights/top-of-mind/gen-ai-too-much-spend-too-little-benefit"&gt;more&lt;/a&gt; &lt;a href="https://www.economist.com/business/2025/05/21/welcome-to-the-ai-trough-of-disillusionment"&gt;commentators&lt;/a&gt; &lt;a href="https://paulkedrosky.com/honey-ai-capex-ate-the-economy/"&gt;talk&lt;/a&gt; about and warn of an “AI bubble”, and everybody seems to congratulate each other on being such a smart financial analyst. BUT: A bubble pops and you are left with air and maybe a splash of soap somewhere on the floor. A fairly clean affair. This kind of investor speak obscures the severe consequences economic crashes cause, coming from someone’s point of view for whom this is more likely to be a spectacle than a direct threat.&lt;/p&gt;



&lt;p&gt;When the “AI” market crashes, there will be NO “reset button”, NO “rollercoaster” continuing on an orderly path after having come down, NO “bubble” that just lets off hot air. These are all metaphors that heavily misrepresent what it means for markets to crash, or, as they say, “correct”. We might be in for a long and painful struggle to at least reduce the grip of “AI” on current core societal functions like government administration, education and research funding. In this article, I want to illustrate the broad range of costs that BOTH the buildup of “AI” overvaluations AND their coming down will have. The current “AI” investments will have long-term costs by creating significant path dependencies: They make harmful things cheaper, speed up the commodification of human labour and shift social norms. Just to be clear: I am referring to the current “AI” boom which is driven mostly by generative AI (“genAI”) applications, not necessarily the things that have been around for decades (e.g. various forms of pattern recognition) and that did not induce companies to spend hundreds of billions on data centres.&lt;/p&gt;



&lt;p&gt;To better understand what is going on, let’s first look at the outcomes of previous instances of overinvestment, including the 2000 dot-com “bubble” and the significant piles of money Uber burnt for many years, before turning to contemporary “AI” path dependencies.&lt;/p&gt;



&lt;h3 class="wp-block-heading has-custom-pink-color has-text-color has-link-color wp-elements-b1db5c78d832fe90a6c94a3d6c122255"&gt;&lt;strong&gt;&lt;strong&gt;Overinvestments shape technological paths&lt;/strong&gt;&lt;/strong&gt;&lt;/h3&gt;



&lt;p&gt;Let’s start with the obvious: The so-called dot-com boom crashed between 2000 and 2002 – this already hints at the fact that “bubble bursting” is a long period during which no one knows when it will end. When it did end, investors lost money, and it is mostly their perspective that was &lt;a href="https://www.businessinsider.com/speculative-bubble-lessons-stock-crypto-outlook-dotcom-era-henry-blodget-2021-11"&gt;covered&lt;/a&gt; in the media (and they whined about a crash being less bad than the pain inflicted by missing out on a boom). Many people lost their jobs, their livelihoods, and needed to find other ways to make ends meet (&lt;a href="https://www.reddit.com/r/programming/comments/1cgf1fd/ask_hn_what_was_the_job_market_like_during_the/"&gt;developers on Reddit&lt;/a&gt; gave an account, but they were probably among the more privileged). Unemployment in the US increased from about 4% to almost 6%.&lt;br&gt;What might be a little less obvious: A few key developments that the dot-com boom had started persisted long after. While the internet was still a mostly academic affair until the 1990s, the dot-com boom kicked off the scale-over-everything, ad-based internet we know today. We saw &lt;a href="https://manifold.umn.edu/read/profit-over-privacy/section/ee270b37-d3d9-4312-b318-57ea01c2328f"&gt;alignment of advertising business and finance&lt;/a&gt; as well as a massive drive to consolidation during the crash. Google, eBay, Amazon, Nvidia, they all became central players in the commercial internet. What now seems inevitable to most people seemed coincidental before the boom – but then today’s driving forces crowded out most other, less commercial forms of existing on the internet.&lt;/p&gt;



&lt;hr class="wp-block-separator has-text-color has-custom-pink-color has-alpha-channel-opacity has-custom-pink-background-color has-background is-style-wide"&gt;&lt;h2 class="wp-block-heading has-text-align-center has-custom-pink-color has-text-color has-link-color wp-elements-f4e142a27545cd194d5a3365a0e90d36"&gt;“AI” investments make harmful things cheaper, speed up the commodification of human labour and shift social norms.&lt;/h2&gt;



&lt;hr class="wp-block-separator has-text-color has-custom-pink-color has-alpha-channel-opacity has-custom-pink-background-color has-background is-style-wide"&gt;&lt;p&gt;The investment logic has shaped the internet ever since: Uber accumulated almost $34bn USD in losses (excluding losses while it was not yet public between 2009 and 2014) before it started to generate profits in &lt;a href="https://www.theguardian.com/technology/2024/feb/07/landmark-moment-as-uber-unveils-first-annual-profit-as-limited-company"&gt;2023&lt;/a&gt;. (And also various &lt;a href="https://wolfstreet.com/2021/07/05/todays-unicorns-have-bigger-cumulative-losses-than-amazon-had-lost-money-far-longer-than-amazon-still-dont-show-a-turnaround/"&gt;other unicorns&lt;/a&gt; incur massive losses they may never recoup.) Money also creates habits and legitimises actions: Uber “&lt;a href="https://www.theguardian.com/news/2022/jul/10/uber-files-leak-reveals-global-lobbying-campaign"&gt;broke laws, duped police and secretly lobbied governments&lt;/a&gt;”, as the Guardian titled in 2022 and its controversies got their own &lt;a href="https://en.wikipedia.org/wiki/Controversies_surrounding_Uber"&gt;Wikipedia page&lt;/a&gt;. But also their very official business model is based on a) normalising precarious working conditions by eroding labour protections as they fought countless lawsuits over giving their workers only freelance and not an employee status, thereby avoiding sick pay, holidays etc., and b) making human work seem more automated and less visible by mediating passengers and drivers through an algorithm in an app, reducing the need for actual human interaction. Both developments shift social norms in ways that are likely to be profitable for businesses even beyond Uber: &lt;strong&gt;Uber’s mission can be understood as reducing the value of workers and human interaction, and with that long-term goal it is commercially rational to rack up significant losses in the short to medium term.&lt;/strong&gt;&lt;/p&gt;



&lt;h3 class="wp-block-heading has-custom-pink-color has-text-color has-link-color wp-elements-ef31312ad6f1dee58b7bf2382857e854"&gt;&lt;strong&gt;The “AI” path dependencies we will not correct&lt;/strong&gt;&lt;/h3&gt;



&lt;p&gt;It is plausible to expect something similar to happen with “AI”. The ongoing investment boom is creating significant overcapacity with effects lasting long after many “AI” startups have gone bust. These range from very visible and direct to the more indirect and structural.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Lower costs for compute and energy&lt;/strong&gt;: In order to sustain the boom, investments follow projections of endless “AI” growth, which translate into hundreds of billions currently being invested into data centre construction, alongside an expansion of energy infrastructure. And once they are built, it does not make sense to stop them, does it? Keeping them running is much cheaper than constructing them. This puts us on a path of energy-intensive technology even once these data centres are no longer needed for “AI” applications. This eradicates any incentive for resource-efficient coding or low-computation technology. At the same time, this infrastructure is not costless to maintain (to my knowledge, chips need to be replaced about every 7 years) – but possibly that cost is still lower than doing anything else, hence continuing on that trajectory will remain cheaper than alternatives for quite some time to come. These artificially low costs of compute and energy will be even further away from the “real” costs when factoring in just the environmental harms they produce. That is very bad news for anybody still hoping for a combined digital and environmental transition.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Sectoral knowledge destruction&lt;/strong&gt;: Some people may get used to using “AI” for a variety of tasks, even where this is neither actually helpful nor profitable for the “AI” providers. As is widely reported, managers often &lt;a href="https://www.telegraph.co.uk/business/2025/07/21/bosses-warn-workers-use-ai-or-face-the-sack/"&gt;encourage&lt;/a&gt; or &lt;a href="https://www.pcgamer.com/gaming-industry/ai-is-no-longer-optional-microsoft-is-allegedly-pressuring-employees-to-use-ai-tools-through-manager-evaluations/"&gt;force&lt;/a&gt; their employees to e.g. code using genAI applications, &lt;a href="https://www.nature.com/articles/s41598-025-92937-2"&gt;university students use genAI&lt;/a&gt; applications for writing, public bodies are continuing to move onto fancy “AI” clouds and &lt;a href="https://berthub.eu/articles/posts/our-self-inflicted-cloud-crisis/"&gt;forget how to do on-premise computing&lt;/a&gt;, and we are likely to see more diffusion before the boom crashes. Just as Uber’s mission was broader than just individual transport, “AI” has an inbuilt contempt for human interaction (as it is built to automate speech while avoiding any interpersonal &lt;a href="https://tante.cc/2025/07/30/friction-and-not-being-touched/"&gt;friction&lt;/a&gt;) and workers (as it seeks to make them even more interchangeable and subordinate to machine processes). Hence, using “AI” often destroys established processes of developing skills and sharing knowledge. Rebuilding them will take much longer and possibly cost more than might have been saved in the meantime.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Again more economic inequality&lt;/strong&gt;: An economic crisis is not equally dangerous for everyone. Only few companies are benefitting from the “AI” boom and most of the stock market gains in recent years were driven by Big Tech valuations going absolutely through the roof. However, we can expect that the losses will be shared more widely, based on the experience of past financial crises. Big Tech is trying to portray itself as &lt;a href="https://ainowinstitute.org/publications/research/1-2-too-big-to-fail-infrastructure-and-capital-push"&gt;too big to fail&lt;/a&gt;, which means that their &lt;a href="https://www.wheresyoured.at/ai-is-a-money-trap/"&gt;systemic relevance&lt;/a&gt; would prompt governments to inject tax money to reduce any losses they might incur. A financial crisis has ripple effects that go far beyond the market in question – just as the 2008 US housing crisis did not only lead to people losing their homes, but caused a huge &lt;a href="https://www.federalreservehistory.org/essays/great-recession-and-its-aftermath"&gt;recession&lt;/a&gt; with a stark increase in unemployment and financial instability.  &lt;/p&gt;



&lt;h3 class="wp-block-heading has-custom-pink-color has-text-color has-link-color wp-elements-bec4897a635f69b0ad9ad50bc79da3ef"&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;Why “AI” overinvestments might take a long time to unwind&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/h3&gt;



&lt;p&gt;There is no way of knowing when the “AI” boom is likely to crash – that is the whole point of markets that are supposed to create collective rationality from individual choices. I see a few reasons to suspect an even longer and more painful struggle than the dot-com crash in 2000. First, the boom is &lt;a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5377426"&gt;orchestrated&lt;/a&gt; or arguably &lt;a href="https://www.tandfonline.com/doi/full/10.1080/09692290.2024.2365757"&gt;planned&lt;/a&gt; by a handful of extremely powerful companies. Being few increases the scope to act strategically. Second, these companies are very close not only to the US government, but through their start-up investments also to governments across the world, selling “AI” promises and lies to politicians. The push of small and large “AI” firms into military tech aggravates this dynamic: It is the area in which talking of an “AI race” carries quite intuitive meaning because having more destructive power translates into military power, though not necessarily into better societal outcomes. And third, the intention of reaching systematic relevance is bearing some fruit as more and more institutions are becoming financially invested into “AI success”. Not only VC investors, but &lt;a href="https://www.noahpinion.blog/p/will-data-centers-crash-the-economy"&gt;large parts of society including life insurance and pension funds&lt;/a&gt; will bear the cost of its failure, giving them an incentive to prolong the boom at fairly high costs.&lt;/p&gt;



&lt;hr class="wp-block-separator has-text-color has-custom-pink-color has-alpha-channel-opacity has-custom-pink-background-color has-background is-style-wide"&gt;&lt;h2 class="wp-block-heading has-text-align-center has-custom-pink-color has-text-color has-link-color wp-elements-345884f2ed11f719063a6e54d5330c77"&gt;There are a few reasons to suspect an even longer and more painful struggle than the dot-com crash: market concentration, closeness to governments, and financial actors being invested into &lt;strong&gt;&lt;strong&gt;&lt;strong&gt;“&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;AI success&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;”&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;.&lt;/h2&gt;



&lt;hr class="wp-block-separator has-text-color has-custom-pink-color has-alpha-channel-opacity has-custom-pink-background-color has-background is-style-wide"&gt;&lt;h3 class="wp-block-heading has-custom-pink-color has-text-color has-link-color wp-elements-5e90c772184a31765a55f8b88dde9ff6"&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;What to do and why not to despair&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/h3&gt;



&lt;p&gt;It is important to analyse the abyss, but don’t stare into the abyss, as Jathan Sadowski sometimes says on my currently favourite podcast &lt;a href="https://soundcloud.com/thismachinekillspod"&gt;This Machine Kills&lt;/a&gt;. Understanding what is happening is essential to figure out a plan and I am keen to do that with others (i.e. I am aware my suggestions are insufficient). Anything that contributes to not making the boom bigger than it needs to be is helpful (e.g. do not invest into “AI”, tell your friends not to, do not use genAI applications or pay for them). Anything that helps us to talk in less delusional terms about what is going on is helpful (e.g. do not join those talking about “bubbles” suggesting they are merely financial events or that have one moment of coming down after which everything will be okay again). And let’s try to preserve that knowledge that companies are keen to replace with “AI”. We will need it.&lt;/p&gt;



&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@nampoh?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Maxim Hopman&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/red-and-blue-light-streaks-fiXLQXAhCfk?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://www.structural-integrity.eu/wp-content/uploads/2025/08/maxim-hopman-fiXLQXAhCfk-unsplash-scaled.jpg"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://www.structural-integrity.eu/feed/</id>
            <title type="html">Structural Integrity</title>
            <link href="https://www.structural-integrity.eu" rel="alternate" type="text/html"/>
            <updated>2026-04-27T08:11:06Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19dc5b77d61:2d20d7:348926d</id>
        <title type="html">Wrap Text Around Images with CSS shape-outside</title>
        <published>2026-04-25T17:37:09Z</published>
        <updated>2026-04-25T17:37:14Z</updated>
        <link href="https://theosoti.com/short/wrap-text-around-images/" rel="alternate" type="text/html"/>
        <summary type="html">Use shape-outside in CSS to wrap text around custom image shapes—no JavaScript, just clean, creative layout control.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;article&gt;&lt;source type="image/webp"&gt;&lt;img src="https://theosoti.com/short/07-2025/shape-outside-image.avif" alt="Text wrapping tightly around the shape of an image using CSS shape-outside and float for a refined layout"&gt;&lt;/source&gt;&lt;h2 id="make-your-text-wrap-around-images-perfectly"&gt;Make your text wrap around images perfectly.&lt;/h2&gt;
&lt;p&gt;By default, text wraps around a boring rectangle.
But what if you could make it hug the actual shape of the image?&lt;/p&gt;
&lt;p&gt;You can.
With just one CSS property:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shape-outside: url(your-img.png);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Add a &lt;code&gt;float&lt;/code&gt; and a bit of margin with &lt;code&gt;shape-margin&lt;/code&gt;,
and your layout feels instantly more refined.&lt;/p&gt;
&lt;p&gt;No JavaScript. No layout hacks.
Just native CSS support and it’s supported in over 95% of browsers.&lt;/p&gt;
&lt;p&gt;Use it with transparent PNGs, SVGs, or even basic shapes like &lt;code&gt;circle()&lt;/code&gt; or &lt;code&gt;polygon()&lt;/code&gt;.
Ideal for editorial layouts, landing pages, or any design that needs more personality.&lt;/p&gt;
&lt;p&gt;Checkout the codepen: &lt;a href="https://codepen.io/theosoti/pen/ogjjged"&gt;https://codepen.io/theosoti/pen/ogjjged&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shape-outside&lt;/code&gt; can produce elegant editorial layouts when image silhouettes stay simple. Test long paragraphs and varied image ratios, because float-based wrapping can become fragile in edge cases.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shape-outside&lt;/code&gt; can create editorial layouts with strong flow, but test with varied image sizes and long text. Float-based wrapping can break faster than block layouts.&lt;/p&gt;
&lt;p&gt;Use this as a readability tool, not only a visual effect. The best result is when style improves scanning speed without adding cognitive load.&lt;/p&gt;
&lt;p&gt;To roll this out safely, start by applying &lt;code&gt;shape-outside: url(your-img.png);&lt;/code&gt; in a single UI surface where the benefit is obvious. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.&lt;/p&gt;
&lt;p&gt;Before shipping, test &lt;code&gt;shape-outside: url(your-img.png);&lt;/code&gt; with both short and long content, then verify behavior in narrow and wide containers.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.&lt;/p&gt;
&lt;p&gt;Go check it out &lt;a href="https://theosoti.com/you-dont-need-js/"&gt;https://theosoti.com/you-dont-need-js/&lt;/a&gt; and enjoy 20% OFF for a limited time!&lt;/p&gt;  &lt;/article&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://theosoti.com/short/07-2025/shape-outside-image.avif"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19dbdc617f2:2a260d:93a1d9ea</id>
        <title type="html">Why The Split? - MeshCore Blog</title>
        <published>2026-04-24T04:36:09Z</published>
        <updated>2026-04-24T04:36:13Z</updated>
        <link href="https://blog.meshcore.io/2026/04/23/the-split" rel="alternate" type="text/html"/>
        <summary type="html">Migrating to the new meshcore.io site</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
        &lt;p&gt;Since inception, the MeshCore development team have been working hard to build MeshCore.&lt;/p&gt;

&lt;p&gt;We’ve released more than 85 versions of the MeshCore Companion, Repeater and Room Server firmwares with support for more than 75 hardware variants.
All of this has been hand crafted, by humans.&lt;/p&gt;

&lt;p&gt;We have always been wary of AI generated code, but felt everyone is free to do what
they want and experiment, etc. But, one of our own, Andy Kirby, decided to branch out
and extensively use Claude Code, and has decided to aggressively take over
all of the components of the MeshCore ecosystem: standalone devices, mobile app, 
web flasher and web config tools.&lt;/p&gt;

&lt;p&gt;And, he’s kept that &lt;em&gt;small&lt;/em&gt; detail a secret - that it’s all majority &lt;em&gt;vibe coded&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;We ran a poll recently, and asked in the MeshCore Discord about AI and trust, and these are the results:&lt;/p&gt;

&lt;p&gt;&lt;img alt="" src="https://blog.meshcore.io/assets/images/2026/04/23/trust-ai-gen-firmware.png"&gt;&lt;/p&gt;

&lt;p&gt;&lt;img alt="" src="https://blog.meshcore.io/assets/images/2026/04/23/have-right-to-know.png"&gt;&lt;/p&gt;

&lt;p&gt;The team didn’t feel it was our place to protest, until we recently discovered that Andy
applied for the MeshCore Trademark (on the 29th March, according to filings) and didn’t tell
any of us. We have tried discussing this, and what his intentions are, but those broke down
and we now have no communication with Andy.&lt;/p&gt;

&lt;p&gt;It’s been a stressful few months trying to sort this out, and is now a sad day
to bring this out to the public. It’s been a slap in the face to the team that
have worked so hard on this project, to have an insider team up with a robot
and a lawyer.&lt;/p&gt;

&lt;h2 id="official-meshcore"&gt;“Official” MeshCore&lt;/h2&gt;

&lt;p&gt;The use of the ‘official’ status is what is currently being contested. Andy is adamant 
that he &lt;em&gt;owns&lt;/em&gt; the brand, and is using the word very heavily with his MeshOS line.&lt;/p&gt;

&lt;p&gt;Meanwhile, in reality, the only ‘official’ MeshCore is the github repo. It’s the
&lt;em&gt;source of truth&lt;/em&gt; in terms of what is MeshCore, and Andy has &lt;em&gt;never&lt;/em&gt; contributed
to that.&lt;/p&gt;

&lt;p&gt;Since the internal split, we launched the &lt;a href="https://meshcore.io"&gt;meshcore.io&lt;/a&gt; site, as Andy controls
the meshcore.co.uk site and original discord server. We’ve been left with little other recourse. And, since 
launching the site, Andy copied the look and feel (again, using Claude) even though
we asked him not to.&lt;/p&gt;

&lt;h2 id="project-growth"&gt;Project Growth&lt;/h2&gt;

&lt;p&gt;The MeshCore project has been on an incredible journey.&lt;/p&gt;

&lt;p&gt;Having only started in January 2025, we have grown extremely fast!&lt;/p&gt;

&lt;p&gt;As of this post, the official &lt;a href="https://map.meshcore.io"&gt;MeshCore Map&lt;/a&gt; shows 38,000+ nodes around the world, and the official &lt;a href="https://meshcore.io"&gt;MeshCore App&lt;/a&gt; has more than 100,000+ active users across Android and iOS.&lt;/p&gt;

&lt;p&gt;It’s pretty epic how we’ve all built such an incredible community in such as a short time!&lt;/p&gt;

&lt;p&gt;As the project grows, so does our need for a dedicated space that provides you with official information from the &lt;em&gt;core team&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In recent times, we’ve seen an explosion of growth in MeshCore web sites dedicated to specific countries and mesh communities.&lt;/p&gt;

&lt;p&gt;To name a few, we’ve seen:&lt;/p&gt;



&lt;p&gt;Andy Kirby did do an amazing job helping to promote the MeshCore project on his personal YouTube, but only promotes his own products now.&lt;/p&gt;

&lt;h2 id="where-to-from-here"&gt;Where To From Here?&lt;/h2&gt;

&lt;p&gt;So, the core team are pushing ahead with the &lt;a href="https://meshcore.io"&gt;meshcore.io&lt;/a&gt; website, the ongoing work of firmware feature development,
bug fixes, managing PR’s and developer discussions, etc.&lt;/p&gt;

&lt;p&gt;We now release change logs, blog posts and technical documentation for all of our new firmware and app releases here.&lt;/p&gt;



&lt;p&gt;You’ll also find some familiar faces on our blog posts, such as:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Scott&lt;/strong&gt; our project founder, lead firmware engineer and developer of the Ripple firmware!&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Recrof&lt;/strong&gt; our official MeshCore Map developer and Firmware Flasher guru. He has shared some insights into the early development of the MeshCore Map.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Liam Cottle&lt;/strong&gt; the official MeshCore App developer who will be posting useful guides for getting started with the MeshCore App.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;FDLamotte&lt;/strong&gt; who has done epic work on the Python tooling for MeshCore, as well as the STM32 firmware variants.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Oltaco&lt;/strong&gt; (Che Aporeps) who has done amazing work on the new OTA Fix bootloader that makes firmware updates much more reliable.&lt;/li&gt;
&lt;/ul&gt;&lt;h2 id="the-core-team"&gt;The Core Team&lt;/h2&gt;

&lt;p&gt;The MeshCore team, now consisting of &lt;strong&gt;Scott&lt;/strong&gt;, &lt;strong&gt;Liam&lt;/strong&gt;, &lt;strong&gt;Recrof&lt;/strong&gt;, &lt;strong&gt;FDLamotte&lt;/strong&gt; and now &lt;strong&gt;Oltaco&lt;/strong&gt; remain committed to designing and developing high quality, &lt;em&gt;human-written&lt;/em&gt; software.&lt;/p&gt;

&lt;h2 id="our-new-home"&gt;Our New Home&lt;/h2&gt;

&lt;p&gt;Please update your bookmarks!&lt;/p&gt;

&lt;p&gt;This is where we will be hosting all official releases, technical documentation, and community discussions moving forward.&lt;/p&gt;

&lt;p&gt;With the new website, we are also starting fresh with a new Discord server!&lt;/p&gt;

&lt;p&gt;This is where you can interact directly with the MeshCore developers, get help with your projects, and contribute to the future of MeshCore.&lt;/p&gt;



&lt;p&gt;Thanks for being a part of this journey!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The MeshCore Team&lt;/em&gt;&lt;/p&gt;

    &lt;/div&gt;

    

&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://blog.meshcore.io/assets/images/icon.png"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://blog.meshcore.io/feed.xml</id>
            <title type="html">blog.meshcore.io</title>
            <link href="https://blog.meshcore.io" rel="alternate" type="text/html"/>
            <updated>2026-04-24T04:36:13Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19dba76e5d6:36e5c9:335607a2</id>
        <title type="html">How to Stop A Data Center in Your Backyard ~ L.A. TACO</title>
        <published>2026-04-23T13:10:47Z</published>
        <updated>2026-04-23T13:10:51Z</updated>
        <link href="https://lataco.com/stop-sgv-data-center-building" rel="alternate" type="text/html"/>
        <summary type="html">These are lessons from San Gabriel Valley neighbors and activists who outsmarted developers and lobbyists.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;When the people of Monterey Park found that their local government was going to approve a &lt;a href="https://www.datacenterdynamics.com/en/news/proposal-for-250000-sq-ft-data-center-in-monterey-park-california-facing-opposition/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;250,000&lt;/strong&gt;&lt;/a&gt;-square-foot data center just 500 feet from their homes, they organized. &lt;/p&gt;&lt;p&gt;And within a few months, the developer withdrew their application.&lt;/p&gt;&lt;p&gt;Andrew Yip, an organizer with&lt;a href="https://www.sgvprogressiveaction.org/" target="_blank" rel="noreferrer noopener"&gt; &lt;strong&gt;SGV Progressive Action&lt;/strong&gt;&lt;/a&gt;, tells L.A. TACO that the organization’s success started with their “existing network of volunteers,” noting that “the community was able to jump in at a moment's notice.” &lt;/p&gt;&lt;p&gt;SGV Progressive Action was founded in 2020 to “address the Black Lives Matter uprisings," Yip says. "To support our Black community." &lt;/p&gt;&lt;p&gt;Then it organized &lt;a href="https://lapublicpress.org/2024/05/ceasefire-resolutions-have-spread-across-san-gabriel-valley-and-southeast-la/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;local resolutions&lt;/strong&gt;&lt;/a&gt; advocating for a ceasefire in Palestine, and built a lending library in El Monte called &lt;a href="https://www.instagram.com/matilijacollective/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;Matilija&lt;/strong&gt; &lt;strong&gt;Collective&lt;/strong&gt;&lt;/a&gt;, where they trained volunteers in community defense against ICE, hosted organizers, and stored 20 canopies and a speaker system. &lt;/p&gt;&lt;p&gt;"So that existed," Yip says.&lt;/p&gt;&lt;p&gt;In November, a community member who had come to a council meeting for other business saw the data center on the agenda and called on SGV Progressive Action. &lt;/p&gt;&lt;p&gt;"They asked if we can take a look at this," Yip says. "And see if that's something that communities should be concerned about."&lt;/p&gt;&lt;p&gt;All that was needed was one last council vote. But the developer requested a delay to the next meeting. &lt;/p&gt;&lt;p&gt;"Had they voted that day, it would have been done, right? It would have been done," Yip says. “But we found out about it, and we turned out hundreds of people to the next meeting.”&lt;/p&gt;&lt;h3 class="wp-block-heading"&gt;CA PUBLIC RECORDS ACT&lt;/h3&gt;&lt;p&gt;Under &lt;a href="https://oag.ca.gov/open-meetings" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;California's Sunshine Laws&lt;/strong&gt;&lt;/a&gt;, local governments are required to turn over agendas, meeting minutes, attendance records, and emails. SGV Progressive Action immediately filed public records requests. &lt;/p&gt;&lt;p&gt;"That's really how we found out," Yip says.&lt;/p&gt;&lt;p&gt;The records showed city planners had given their &lt;a href="https://www.montereypark.ca.gov/DocumentCenter/View/16863/1977-Saturn-Data-Center---Notice-of-Intent-to-Adopt-a-Mitigated-Negative-Declaration" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;blessings&lt;/strong&gt;&lt;/a&gt; to the data center, saying it would not result in any “significant environmental impacts.” They had used the developer's own impact &lt;a href="https://ceqanet.lci.ca.gov/2024101397#:~:text=The%20Project%20would%20demolish%20the,of%20mechanical%20equipment%20platform%20screening)." target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;assessment&lt;/strong&gt;&lt;/a&gt; in place of a more thorough state environmental review. &lt;/p&gt;&lt;p&gt;The records also showed the city had held a series of community meetings to ask residents what should be built at 1977 Saturn Street, but only notified people living within 500 feet. Each meeting drew between 20 and 60 people. Residents who were there told Yip and other organizers that the city clerk brought in people who backed the data center. It won with roughly 20 votes.&lt;/p&gt;&lt;p&gt;“Twenty-something votes determined [that] residents here wanted a data center,” Yip says. ”That just seemed like a weird recommendation coming out of a community town hall.”&lt;/p&gt;&lt;p&gt;Council Member Thomas Wong, who would vote for having the data center, also works at the power company that would sell its electricity.&lt;/p&gt;&lt;p&gt;The developer also bought a larger property at 1980 Saturn Street across the street. The data center trade magazines &lt;a href="https://www.latimes.com/b2b/ai-technology/story/2024-12-20/vacant-monterey-park-office-building-to-be-converted-into-data-center" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;reported&lt;/strong&gt;&lt;/a&gt; that these were part of a 13-parcel assembly, all meant for data centers. Yip asked the developers what they planned to do with 1980 Saturn; they said they were not authorized to discuss it.&lt;/p&gt;&lt;h3 class="wp-block-heading"&gt;NO DATA CENTER MPK &amp;amp; THE INFORMATION CAMPAIGN&lt;/h3&gt;&lt;p&gt;The residents of Monterey Park bought the domain &lt;a href="https://www.nodatacentermpk.org/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;No Data Center MPK&lt;/strong&gt;&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;“And we had a ton of people come out to support, whether it's walking the neighborhoods, distributing fliers, calling folks, creating artwork,” Yip says. “It was a big showing.”&lt;/p&gt;&lt;p&gt;They went door-to-door to tell their neighbors what the city had not told them: The data center would use twice as much electricity as all of Monterey Park. The 14 “backup” &lt;a href="https://lapublicpress.org/2026/01/a-data-center-boom-is-coming-to-the-san-gabriel-valley-residents-had-no-idea/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;generators&lt;/strong&gt;&lt;/a&gt; would burn 200,000 gallons of diesel every year, without a blackout. And more when the grid price was high.&lt;/p&gt;&lt;p&gt;They held a teach-in. 150 people came. &lt;/p&gt;&lt;p&gt;“We have a lot of very smart residents who were able to do a lot of this research and fact-finding. Many of the residents we work with are researchers or hold PhDs, and they work in universities. So they know how to find this information,” Yip says. &lt;/p&gt;&lt;p&gt;One resident 3D-printed a model of the data center to show just how much of the neighborhood’s space it would take up. Another resident mixed noise recordings from data centers and played them over a loudspeaker. You couldn't hear the birds. &lt;/p&gt;&lt;p&gt;Two dozen people in Virginia who lived near a data center were ready to fly out to testify on their own dime. &lt;/p&gt;&lt;p&gt;“Virginia became ground zero for data center proliferation,” says Yip. “The people didn't know enough about data centers at the time.”&lt;/p&gt;&lt;p&gt;The vibrations and noise never stop, the people from Virginia warned. The reported sound levels of 60db and higher are far from the data center. They have taken to &lt;a href="https://www.businessinsider.com/data-centers-northern-virginia-noise-air-pollution-cost-2025-5" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;sleeping&lt;/strong&gt;&lt;/a&gt; in their basements. Neither a decibel meter nor the law measures vibration.&lt;/p&gt;&lt;p&gt;Yip and the organizers found that almost nobody in Monterey Park that they spoke to had heard of the data center. The city’s notification had only reached 40 people living within 500 feet of its proposed location, in English. &lt;/p&gt;&lt;p&gt;The neighborhood is 65 percent Asian and 27 percent Latino. The community’s outreach was done in at least three languages, five when necessary. It extended to all the surrounding neighborhoods.&lt;/p&gt;&lt;p&gt;“Data centers, their pollution, and their effects don't just stop at the border,” Yip says.&lt;/p&gt;&lt;p&gt;They started a petition in English, Chinese, and Spanish. It grew to 4,500 signatures.&lt;/p&gt;&lt;h3 class="wp-block-heading"&gt;THE DEVELOPERS &amp;amp; THE DISINFORMATION CAMPAIGN&lt;/h3&gt;&lt;p&gt;The developers retained a law firm with 1,000 attorneys on four continents. They &lt;a href="https://www.sgvtribune.com/2026/03/03/monterey-park-data-center-plan-back-in-front-of-city-council/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;hired&lt;/strong&gt;&lt;/a&gt; &lt;a href="https://actumllc.com/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;Actum&lt;/strong&gt;&lt;/a&gt;, the lobbying firm that represents Amazon and Clorox. Actum lists Trump’s former chief of staff as a &lt;a href="https://actumllc.com/people/mick-mulvaney/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;partner&lt;/strong&gt;&lt;/a&gt; and another who exploited a loophole so large that California had to &lt;a href="https://www.sacbee.com/news/politics-government/capitol-alert/article314044368.html" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;legislate&lt;/strong&gt;&lt;/a&gt; to close it.&lt;/p&gt;&lt;p&gt;"The applicant sent a letter to the city council talking about misinformation being spread, and that was exactly what the council members said," Yip tells us. "They just parroted the same talking points."&lt;/p&gt;&lt;p&gt;The political lobbyists canvassed neighborhoods and local shops. &lt;/p&gt;&lt;p&gt;"One of the public benefits of the project they were touting was a pocket park ... The pocket park is basically just leftover land on their property that they didn't really need for the data center," Yip says.&lt;/p&gt;&lt;p&gt;They promised 200 jobs and, in the same conversation, no traffic. &lt;/p&gt;&lt;p&gt;“Our people would poke holes in it,” Yip says. “Why wouldn't there be cars in that facility if you're going to have a lot of employees?”&lt;/p&gt;&lt;p&gt;No Data Center MPK retained an environmental and land use attorney. They recommended an ordinance banning data centers immediately, then to reinforce it with a ballot measure.&lt;/p&gt;&lt;p&gt;They set up a one-click email so residents could send comments to City Council demanding both.&lt;/p&gt;&lt;p&gt;Hundreds showed up to the next three council meetings. &lt;/p&gt;&lt;p&gt;“We played mahjong on the City Hall lawn while we waited. We had a lion dance right outside to cheer people on as they entered the council chambers,” Yip says. &lt;/p&gt;&lt;p&gt;The chambers filled, overflowing into the aisles and hallways.&lt;/p&gt;&lt;p&gt;The first meeting produced a 45-day ban on data centers, the second brought a ballot measure that would ban them forever. &lt;/p&gt;&lt;p&gt;During the third meeting, opponents of the data center called for a rally at 5:30 p.m. before the 6:30 p.m. meeting. Steven Kung, the Monterey Park resident who purchased the No Data Center MPK domain, addressed the developers, their lawyers, and the lobby firm directly.&lt;/p&gt;&lt;p&gt;“You’re fighting an uphill battle against an entire city that doesn’t want you here and yet you continue to bully your way into this community of color, to pollute the air we breathe, to make electricity more expensive, to devalue our homes, to drain our energy and resources like a parasite,” Kung &lt;a href="https://www.instagram.com/reels/DUYeFzXgCYe/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;says&lt;/strong&gt;&lt;/a&gt;. “You think you can take us on? You’ve messed with the wrong city. ”&lt;/p&gt;&lt;p&gt;On March 31, the developer withdrew its application.&lt;/p&gt;&lt;p&gt;“They underestimated the community's passion. And we never underestimated them. And I think that was a good strategy,” says Yip.&lt;/p&gt;&lt;p&gt;The &lt;a href="https://www.ca.gov/departments/235/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;FPPC&lt;/strong&gt;&lt;/a&gt;, California’s political ethics commission, saw the data center would have a “material financial effect” on councilmember Wong. His power to vote on them was &lt;a href="https://www.fppc.ca.gov/siteassets/documents/legal_div/advice_letters/2020-2026/2026/25147.pdf" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;stripped&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Yip says the people who joined for the fight over 1977 Saturn Street have overwhelmingly stayed active with SGV Progressive Action.&lt;/p&gt;&lt;p&gt;"They recognize it's not just about data centers. It's about building community and protecting your community," he says.&lt;/p&gt;&lt;p&gt;The volunteers who organized against the data center are now working to &lt;a href="https://www.instagram.com/p/DVrs4wxDwKk/?img_index=1" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;strengthen&lt;/strong&gt;&lt;/a&gt; sanctuary ordinances to protect their communities from ICE.&lt;/p&gt;&lt;p&gt;“And now we have a coalition of all these community members coming from La Puente, Avocado Heights, Rowland Heights, and Hacienda Heights coming together to fight this common enemy. And I'm just going to name it the City of Industry,” Yip says.&lt;/p&gt;&lt;h3 class="wp-block-heading"&gt;NO DATA CENTERS SGV COALITION&lt;/h3&gt;&lt;p&gt;Samuel Brown Vazquez rode ten miles horseback from Avocado Heights to the Monterey Park council meeting. Vazquez is a community organizer and founding member of the &lt;a href="https://www.instagram.com/avocadoheightsvaqueros/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;Avocado Heights Vaqueros&lt;/strong&gt;&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;The City of Industry is in the process of approving zoning changes that would clear the way for three data centers and battery energy storage that would affect the residents of Hacienda Heights, La Puente, Walnut, Diamond Bar, West Covina and others.&lt;/p&gt;&lt;p&gt;The Avocado Heights Vaqueros, SGV Progressive Action, and others organized across city and county lines. &lt;/p&gt;&lt;p&gt;“We created an infographic and started mobilizing folks,” Vazquez says. They are &lt;a href="https://www.nodatacenterssgvcoalition.org/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;No Data Centers SGV Coalition&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Like Monterey Park, they canvassed door-to-door, showed up to every City of Industry meeting, started a petition, and filed public records requests.&lt;/p&gt;&lt;p&gt;The record requests showed the City of Industry had been &lt;a href="https://www.documentcloud.org/documents/27693914-re-quick-call-07-05-2025-produce/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;discussing&lt;/strong&gt;&lt;/a&gt; zoning changes with developers at Puente Hills Mall, Madrid Middle School, and two battery storage facilities near Hacienda Heights. All just feet from homes and schools.&lt;/p&gt;&lt;p&gt;In February, the city unanimously rezoned Puente Hills Mall for battery storage. Months earlier, the city manager, Joshua Nelson, &lt;a href="https://investigatela.org/2026/03/02/city-of-industry-discussed-data-center-sites-months-before-zoning-vote/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;emailed the developers that&lt;/strong&gt;&lt;/a&gt; they were working to allow data centers anywhere in the city.&lt;/p&gt;&lt;p&gt;Again, organizers found that people had no idea what the city was planning to put next to their homes. Battery centers burned for days when they caught fire. One at Moss Landing had &lt;a href="https://www.youtube.com/watch?v=ooYmpF0utVs" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;spilled&lt;/strong&gt;&lt;/a&gt; 55,000 tons of nickel, cobalt, and lithium.&lt;/p&gt;&lt;p&gt;“Maybe only one or two people had heard," said Sophia Ramirez, an organizer and the daughter of Zacatecan immigrants. “That was pretty shocking.”' &lt;/p&gt;&lt;p&gt;Ramirez, a Cal Poly biology grad, explained in the outreach, in plain language, how PM2.5 and PM10 particles could slip past the body's filters into the lungs, then the blood. &lt;/p&gt;&lt;p&gt;The No Data Center SGV petition grew to 18,000 signatures.&lt;/p&gt;&lt;p&gt;In Monterey Park, the people who organized were also the people who vote. In the City of Industry, there hasn’t been a competitive election in &lt;a href="https://www.latimes.com/local/politics/la-me-industry-audit-20160129-story.html" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;10 years&lt;/strong&gt;&lt;/a&gt;, and only four &lt;a href="https://www.latimes.com/archives/la-xpm-1992-03-22-ga-7418-story.html" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;since 1957&lt;/strong&gt;&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;State law says when fewer people run than there are open seats for, a city doesn't have to hold an election. The City of Industry council appoints its council members and some of its members are descendents of the original founders.&lt;/p&gt;&lt;p&gt;The City of Industry has 256 residents, the largest financial reserves of any city in the San Gabriel Valley, and a &lt;a href="https://www.bibliovault.org/BV.book.epl?ISBN=9780813551920" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;history&lt;/strong&gt;&lt;/a&gt; of building its wealth at the expense of the surrounding working-class Latino and Asian Pacific Islander communities. &lt;/p&gt;&lt;p&gt;"I had normalized what it was like to live next to City of Industry,” Vazquez says. “I thought it was normal to just grow up near all these warehouses."&lt;/p&gt;&lt;p&gt;The people of the surrounding areas, Covina, Diamond Bar, El Monte, La Puente, Pomona, Walnut, and West Covina, would all be affected by the data centers, but have no political power over the City of Industry.&lt;/p&gt;&lt;p&gt;“To think of it in the context of environmental racism, environmental injustices, it’s really crazy that it's like the city of industry has decided that these communities that live around them are not valuable lives,” Ramirez says. “Zonas de sacrificio.”&lt;/p&gt;&lt;p&gt;People from La Puente, Avocado Heights, Rowland Heights, Diamond Bar, Walnut, and Hacienda Heights came to the City of Industry’s March 26 council meeting where they planned to change the city's zoning code to allow data centers. &lt;/p&gt;&lt;p&gt;The city’s email went down before the &lt;a href="https://www.instagram.com/reels/DWXiRobj0OI/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;meeting&lt;/strong&gt;&lt;/a&gt;. People said it often does. The only way to comment is to show up. More than 100 people did, at 9 a.m. on a workday. Before public comment, the council went into closed session. &lt;/p&gt;&lt;p&gt;“They made us all wait outside in the heat,” Ramirez says. &lt;/p&gt;&lt;p&gt;Ramirez said that it was 90 degrees, and there were many elders. After two hours, roughly 40 were let inside. Outside, there was no livestream. They chanted, "Let us in." &lt;/p&gt;&lt;p&gt;When the doors opened, only about 30 people were allowed to speak. Their comment time was cut from three minutes to one minute. Mayor Pro Tem Greubel got up and walked out halfway through.&lt;/p&gt;&lt;p&gt;The City of Industry does not provide interpreters, translated materials, or any way for people who speak other languages to comment. They haven't posted meeting minutes in over a year. More than a quarter of their meetings are called with 24 hours notice.&lt;/p&gt;&lt;p&gt;“Everything about City of Industry is designed to minimize participation of the public,” Vazquez tells us. ”Like, that is not an exaggeration to say that.”&lt;/p&gt;&lt;h3 class="wp-block-heading"&gt;VALLE IMPERIAL RESISTE VS. IMPERIAL COUNTY&lt;/h3&gt;&lt;p&gt;Gilberto Manzanarez, an organizer and founder of &lt;a href="https://www.instagram.com/valleimperialresiste/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;Valle Imperial Resiste&lt;/strong&gt;&lt;/a&gt;, learned of the data center on Facebook. Someone had leaked the planning commission’s map over Thanksgiving break. &lt;/p&gt;&lt;p&gt;Manzanarez grabbed his camera, drove to the site, and &lt;a href="https://www.instagram.com/p/DRnBkV9ktI2/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;posted&lt;/strong&gt;&lt;/a&gt; a video that spread across the valley. &lt;/p&gt;&lt;p&gt;The Imperial Valley data center would be one of the world’s largest at nearly one million square feet. It would use double the electricity of Imperial Valley and 750,000 gallons of water every day. County planning staff decided they “qualified as a permitted industrial use,” and there would be no need for environmental review.&lt;/p&gt;&lt;p&gt;Manzanarez, also a history teacher, says, “Public health always takes a backseat to economic development in the Imperial Valley.” &lt;/p&gt;&lt;p&gt;The &lt;a href="https://www.niehs.nih.gov/research/supported/translational/community/imperial#:~:text=Air%20pollution%20in%20Imperial%20County%20comes%20from%20agricultural%20burns%2C%20diesel,sources%20such%20as%20automobile%20exhaust." target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;air carries&lt;/strong&gt;&lt;/a&gt; cropland burnings, diesel fumes, and dust from the drying Salton Sea, laced with pesticides and heavy metals. More than &lt;a href="https://keck.usc.edu/news/children-living-near-the-salton-sea-in-southern-california-show-slower-lung-function-growth/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;one in five children&lt;/strong&gt;&lt;/a&gt; that live around the Salton Sea have asthma.&lt;/p&gt;&lt;p&gt;Despite decades of investment, solar farms, geothermal plants, military bases, and canals, the 80 percent Latino region’s unemployment sits at &lt;a href="https://labormarketinfo.edd.ca.gov/file/lfmonth/lf_geomaps.pdf" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;three times&lt;/strong&gt;&lt;/a&gt; the national average.&lt;/p&gt;&lt;p&gt;“The promise of economic development and jobs, the same thing, these are stories that we already heard when we had the solar farm boom. Those solar panel projects were sold to us,” Manzanarez says. “It's like, this is going to save us. This is going to lift us out of poverty ... Thousands of people across the Imperial Valley were hired, and they were excited to go work. Fast forward 10 years, 2026, guess what? We have the highest unemployment rate in the state of California. Again.”&lt;/p&gt;&lt;p&gt;Bryan Vega, another local organizer, saw the Valle Imperial Resiste post and joined the other activists. They knocked on doors, handed out flyers, and flooded Instagram with informational videos. &lt;/p&gt;&lt;p&gt;“We started to share information about what the data center is and invited folks to submit public comment,” Vega says.&lt;/p&gt;&lt;p&gt;In January, they held a protest on Main Street and Imperial Avenue. They started a petition to enact the Imperial County Data Center Prohibition Act. &lt;/p&gt;&lt;p&gt;On March 26, the Imperial County Board of Supervisors called for an evening meeting, outside of their normal schedule, specifically about the data center. &lt;/p&gt;&lt;p&gt;The main chamber filled 30 minutes before it started. Two overflow rooms opened in a separate building. And when those filled, too, more than 60 people stood in the parking lot. &lt;/p&gt;&lt;p&gt;The developer, Sebastian Rucci, spoke. There were to be no questions.&lt;/p&gt;&lt;p&gt;Vega recalled standing out in the parking lot. &lt;/p&gt;&lt;p&gt;“Someone outside said, ‘Why are we just standing here? Why are we not more upset? They're making decisions about us in there. And we're out here. And we should be in there. We go to the door and we're like, ‘No Data Center. No Data Center.’ And it's like a battle cry from our soul,” he says.&lt;/p&gt;&lt;p&gt;Vega said one of the organizers, from &lt;a href="https://www.instagram.com/ivforpalestine/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;Imperial Valley for Palestine&lt;/strong&gt;&lt;/a&gt;, had a bullhorn in her car, “because, duh, she's an organizer and she's always prepared for these things."&lt;/p&gt;&lt;p&gt;&lt;em&gt;KPBS&lt;/em&gt; &lt;a href="https://www.kpbs.org/news/environment/2026/04/03/imperial-county-supervisors-to-hold-key-vote-on-controversial-data-center-project" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;reported&lt;/strong&gt;&lt;/a&gt; that “county officials paused the meeting and Rucci departed early after protestors drowned him out.” &lt;/p&gt;&lt;p&gt;Videos &lt;a href="https://www.instagram.com/p/DWZ-xwCkpns/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;show&lt;/strong&gt;&lt;/a&gt; Imperial County sheriff’s deputies escorting Rucci and his business partner, Hector Casas, to their car. &lt;/p&gt;&lt;p&gt;Outside, the 60 people who weren’t allowed in chanted “Fuera! Fuera!” at Rucci and Casas as they drove off.&lt;/p&gt;&lt;p&gt;"This meeting was a sham,” Manzanarez says. “They didn't want to educate us. They didn't want to hear people. They just wanted to check a box."&lt;/p&gt;&lt;p&gt;&lt;em&gt;KPBS&lt;/em&gt; &lt;a href="https://www.kpbs.org/news/environment/2026/01/22/4-takeaways-from-kpbs-investigation-into-a-massive-data-center-project-in-imperial-county" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;reported&lt;/strong&gt;&lt;/a&gt; that in 2010, Ohio prosecutors charged Rucci with money laundering, promoting prostitution, and perjury at a Youngstown nightclub. The felonies were thrown out, but he served 30 days for selling alcohol on an expired license. He later opened an addiction treatment center. The state revoked its certification after finding falsified records. &lt;/p&gt;&lt;p&gt;In 2021, the FBI raided the treatment center and seized more than $600,000. No criminal charges were filed. Rucci sued, got the money back, and is still &lt;a href="https://www.opn.ca6.uscourts.gov/opinions.pdf/25a0306p-06.pdf" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;fighting&lt;/strong&gt;&lt;/a&gt; in federal court. &lt;/p&gt;&lt;p&gt;In an &lt;a href="https://www.kpbs.org/news/environment/2026/01/21/the-plan-to-build-a-massive-data-center-in-imperial-county-without-environmental-review" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;interview&lt;/strong&gt;&lt;/a&gt; with &lt;em&gt;KPBS&lt;/em&gt;, he said, "I won them all but one."&lt;/p&gt;&lt;p&gt;Rucci and his partner, Hector Casas, targeted Imperial County because the zoning laws allowed them to skip environmental review. &lt;/p&gt;&lt;p&gt;"Our whole goal is speed," Rucci &lt;a href="https://www.kpbs.org/news/environment/2026/01/21/the-plan-to-build-a-massive-data-center-in-imperial-county-without-environmental-review" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;told&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;&lt;em&gt;KPBS&lt;/em&gt;. "That is not sneaky. That's just smart."&lt;/p&gt;&lt;p&gt;On April 7, at 8 a.m., 50 men got off a tour bus with identical orange vests that said, "Data centers equal jobs and prosperity." &lt;/p&gt;&lt;p&gt;They were led through the side entrance of the County Administration Building before any members of the community were allowed through the front. A leaked internal county email sent the night before warned of high turnout and increased security. Despite that, the county provided no overflow room. &lt;/p&gt;&lt;p&gt;Over 100 community members were left outside in 96-degree heat for five to six hours. Elders with walkers. No shade, no water, no chairs. One community member got kicked out for calling out the outsiders taking seats from residents.&lt;/p&gt;&lt;p&gt;The board of supervisors voted to approve the lot merger. Only one supervisor voted no.&lt;/p&gt;&lt;p&gt;Senator Padilla's SB 887 would require all new data centers in California to go through environmental review. The bill does not ban data centers, it only requires developers to study their impact and hear from the public before building.&lt;/p&gt;&lt;p&gt;On April 2, the organizers filed the Imperial County Data Center Prohibition Act, the first step toward a ballot measure that would ban data centers across the county. &lt;/p&gt;&lt;p&gt;“Our parents and grandparents sacrificed so much to be able to be in the Imperial Valley ... part of the sacrifice was having to accept a hard hand,“ Vegas says. “Like when I think about what my Mexican farmworker parents had to undergo, it makes it almost intuitive to fight for the environment, intuitive to fight for the things that I know are true to them, but also very simply put, we would not be here without that."&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Julianne Le</name>
        </author>
        <media:content medium="image" url="https://lede-admin.lataco.com/wp-content/uploads/sites/45/2026/04/Document-e1776358247494.jpeg"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19d9992bf3a:36b762:9f9ae75a</id>
        <title type="html">How one programmer's pet project changed how we think about software</title>
        <published>2026-04-17T03:53:44Z</published>
        <updated>2026-04-17T03:54:07Z</updated>
        <link href="https://www.youtube.com/watch?v=Y24vK_QDLFg" rel="alternate" type="text/html"/>
        <summary type="html">This is the story of how one programmer's obsession with simplicity quietly reshaped how the software world thinks about time, immutability, and what it mean...</summary>
        <content type="html">&lt;div&gt;&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/Y24vK_QDLFg"&gt;&lt;/iframe&gt;&lt;br&gt;&lt;p&gt;This is the story of how one programmer's obsession with simplicity quietly reshaped how the software world thinks about time, immutability, and what it means to write code that lasts. From a sabbatical pet-project to the backbone of one of the world's largest fintechs and a global community that treats their language like a philosophy. This is the story of Clojure. 

---

This documentary wouldn't exist without the kind support of Nubank: https://building.nubank.com/engineering/

Thanks to Railway, our channel sponsor, for supporting all of our films:
Railway is the all-in-one intelligent cloud provider ➡️ https://railway.com/?referralCode=cultrepo

---

The Clojure Documentary features:

Alessandra Sierra, Alex Miller, Chris Houser, David Nolen, Ed Wible, Eric Normand, Eric Thorsen, Lucas Cavalcanti, Michael Fogus, Nathan Marz, Rich Hickey, Steph Hickey, and Stuart Halloway.

Film Credits: 
Directed by: Cormac Dunne
Produced by: Emma Tracey
Additional direction: Joey Bania 
Music supervision and sound design: Tomás Malara

---

For a full overview of all the papers, talks, essays, etc. that appear in the film, check this link: https://clojure.org/about/documentary


---

Follow us:
X: x.com/CultRepo
Bluesky: cultrepo.bsky.social
Instagram: www.instagram.com/cult.repo
LinkedIn: https://www.linkedin.com/company/cult-repo&lt;/p&gt;&lt;/div&gt;</content>
        <author>
            <name>TLDR News EU</name>
        </author>
        <media:content medium="image" url="https://i.ytimg.com/vi/Y24vK_QDLFg/maxresdefault.jpg"/>
        <link href="https://www.youtube.com/embed/Y24vK_QDLFg" rel="enclosure" type="text/html"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19d96383117:a4ef3:e8c372da</id>
        <title type="html">‘By Design’ Flaw in MCP Could Enable Widespread AI Supply Chain Attacks</title>
        <published>2026-04-16T12:15:58Z</published>
        <updated>2026-04-16T12:16:01Z</updated>
        <link href="https://www.securityweek.com/by-design-flaw-in-mcp-could-enable-widespread-ai-supply-chain-attacks/" rel="alternate" type="text/html"/>
        <summary type="html">Researchers warn that a flaw in Anthropic’s Model Context Protocol allows unsanitized commands to execute silently, enabling full system compromise across widely used AI environments.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
		
&lt;p&gt;&lt;strong&gt;Model Context Protocol (MCP) has been a boon to agentic AI users and is widely used and trusted locally by companies adopting agentic AI internally. &lt;/strong&gt;



&lt;/p&gt;&lt;p&gt;Introduced by Anthropic in November 2024, it provides a standard connector between agents and data. Enterprises use it locally to avoid the pain of developing their own connectors, and it is in widespread use as a local STDIO MCP server.



&lt;/p&gt;&lt;p&gt;There are multiple providers of MCP servers, almost all inheriting Anthropic’s code. The problem, &lt;a href="https://20204725.hs-sites.com/the-mother-of-all-ai-supply-chains" target="_blank"&gt;reports&lt;/a&gt; OX Security, is what it terms an architectural flaw in Anthropic’s MCP code embedded within most of these local STDIO MCPs.



&lt;/p&gt;&lt;p&gt;In a nutshell, OX Security says this flaw can result in a complete adversarial takeover over the user’s computer system. “And the exploit mechanism is straightforward, MCP’s STDIO interface was designed to launch a local server process. But the command is executed regardless of whether the process starts successfully,” reports OX.



&lt;/p&gt;&lt;p&gt;“Pass in a malicious command, receive an error – and the command still runs. No sanitization warnings. No red flags in the developer toolchain. Nothing.”



&lt;/p&gt;&lt;p&gt;OX extensively tested whether this ‘flaw’ was exploitable, extensively succeeded, and extensively disclosed its findings to the MCP providers; from Anthropic downward. Initially it had little response. Eventually, the common response was inaction coupled with the suggestion that this behavior was ‘by design’. &lt;/p&gt;&lt;div&gt;&lt;span&gt;Advertisement. Scroll to continue reading.&lt;/span&gt;&lt;/div&gt;



&lt;p&gt;But OX discovered, and demonstrated, that this ‘by design’ behavior could be easily exploited, leaving potentially millions of downstream users exposed to sensitive data, API key and internal corporate data theft, the exposure of chat histories, and more. If the process that MCP failed included malware, that malware could be silently installed, potentially leading to complete system takeover.



&lt;/p&gt;&lt;p&gt;Eventually, the only apparent action from Anthropic was to quietly update its security guidance to recommend MCP adapters be used ‘with caution’ – “leaving the flaw intact and shifting responsibility to developers”.



&lt;/p&gt;&lt;p&gt;This is an interesting position to take. It suggests that developers are responsible for the security of what they develop, which is fair. It possibly also suggests that any company so breached is not the responsibility of Anthropic, but the fault of misconfiguring the MCP installation – which certainly &lt;a href="https://www.securityweek.com/the-wild-wild-west-of-agentic-ai-an-attack-surface-cisos-cant-afford-to-ignore/"&gt;does happen&lt;/a&gt;. And to be fair, GitHub’s own installation was an exception to the OX testing, proving that security gating on installation is possible.



&lt;/p&gt;&lt;p&gt;&lt;a href="https://www.airisksummit.com/" target="_blank"&gt;&lt;strong&gt;Learn More at the AI Risk Summit | Ritz-Carlton, Half Moon Bay&lt;/strong&gt;&lt;/a&gt;



&lt;/p&gt;&lt;p&gt;But the sheer volume of successful compromises conducted by OX demonstrates that the developers installing MCP servers are failing to install successfully. This should be no surprise when AI is automating so many aspects of security and lowering the bar of security competence among developers.



&lt;/p&gt;&lt;p&gt;The OX position is that Anthropic should take responsibility and fix this ‘architectural flaw’ itself. Without doing so, it is leaving industry open to “the mother of all supply chain attacks”, starting from Anthropic, fanning out to many thousands of local MCP users, and from those compromised systems to who knows how many other servers.



&lt;/p&gt;&lt;p&gt;During its research, OX adopted a coordinated disclosure process, leading to more than 30 accepted disclosures and more than 10 high and critical vulnerabilities patched. But the underlying design flaw, it says, remains, leaving millions of users and thousands of systems exposed to unauthorized access. “The current implementation of the Model Context Protocol places the entire burden of security on the downstream developer – a structural failure that guarantees vulnerability at scale.”



&lt;/p&gt;&lt;p&gt;The OX report on its findings includes details on how Anthropic could solve the problem by deprecating unsanitized STDIO connections, introducing protocol level command sandboxing, including a ‘dangerous mode’ explicit opt-in, and developing marketplace verification standards to include a standardized security manifest.



&lt;/p&gt;&lt;p&gt;In the meantime, any company adopting STDIO MCP as part of an agentic AI development should do so ‘with caution’.



&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;: &lt;a href="https://www.securityweek.com/anthropic-mcp-server-flaws-lead-to-code-execution-data-exposure/"&gt;Anthropic MCP Server Flaws Lead to Code Execution, Data Exposure&lt;/a&gt;



&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;: &lt;a href="https://www.securityweek.com/top-25-mcp-vulnerabilities-reveal-how-ai-agents-can-be-exploited/"&gt;Top 25 MCP Vulnerabilities Reveal How AI Agents Can Be Exploited&lt;/a&gt;



&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;: &lt;a href="https://www.securityweek.com/the-new-rules-of-engagement-matching-agentic-attack-speed/"&gt;The New Rules of Engagement: Matching Agentic Attack Speed&lt;/a&gt;



&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;: &lt;a href="https://www.securityweek.com/anthropic-unveils-claude-mythos-a-cybersecurity-breakthrough-that-could-also-supercharge-attacks/"&gt;Anthropic Unveils ‘Claude Mythos’ – A Cybersecurity Breakthrough That Could Also Supercharge Attacks&lt;/a&gt;
			&lt;/p&gt;&lt;/div&gt;
	&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Kevin Townsend</name>
        </author>
        <media:content medium="image" url="https://www.securityweek.com/wp-content/uploads/2026/04/MCP_Vulnerability.jpg"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://www.securityweek.com/feed/</id>
            <title type="html">SecurityWeek</title>
            <link href="https://www.securityweek.com" rel="alternate" type="text/html"/>
            <updated>2026-04-16T12:16:01Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19d95ec6bfd:3e533:44dea31a</id>
        <title type="html">font-family Doesn’t Fall Back the Way You Think</title>
        <published>2026-04-16T10:53:12Z</published>
        <updated>2026-04-16T10:53:17Z</updated>
        <link href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/" rel="alternate" type="text/html"/>
        <summary type="html">A quick but important reminder that font-family declarations don’t inherit fallback stacks the way many developers assume.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;

        

        &lt;p&gt;
          &lt;time datetime="2026-04-10T11:30:00+00:00"&gt;10 April, 2026&lt;/time&gt;&lt;/p&gt;

          &lt;h1&gt;font-family Doesn’t Fall Back the Way You Think&lt;/h1&gt;

          &lt;p&gt;Written by &lt;b&gt;Harry Roberts&lt;/b&gt; on &lt;b&gt;CSS Wizardry&lt;/b&gt;.&lt;/p&gt;

          

          
            &lt;details&gt;&lt;summary&gt;Table of Contents&lt;/summary&gt;&lt;p&gt;Independent writing is brought to you via my wonderful
                  &lt;a href="https://csswizardry.com/supporters/"&gt;Supporters&lt;/a&gt;.&lt;/p&gt;

                &lt;ol&gt;&lt;li&gt;&lt;a href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/#font-family-fallbacks-are-self-contained"&gt;&lt;code&gt;font-family&lt;/code&gt; Fallbacks Are Self-Contained&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/#why-you-get-a-flash-of-times"&gt;Why You Get a Flash of Times&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/#the-fix-is-simple"&gt;The Fix Is Simple&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/#why-this-matters"&gt;Why This Matters&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/#closing-thoughts"&gt;Closing Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/details&gt;&lt;p&gt;There is a small but surprisingly important nuance in the way &lt;code&gt;font-family&lt;/code&gt;
works that seems to catch a lot of people out. In my continuing series on &lt;a href="https://csswizardry.com/2026/04/what-is-css-containment-and-how-can-i-use-it/"&gt;web
performance&lt;/a&gt; for &lt;a href="https://csswizardry.com/2026/03/when-all-you-can-do-is-all-or-nothing-do-nothing/"&gt;design
systems&lt;/a&gt;, today
we’ll look at font stacks and how, when improperly configured, they can cause
unsightly flashes of inappropriate or unexpected fallback text, and in more
extreme cases, layout shifts.&lt;/p&gt;

&lt;p&gt;Correctly, developers for the most part know that &lt;code&gt;font-family&lt;/code&gt; is an inherited
property: set a font family on the &lt;code&gt;:root&lt;/code&gt;/&lt;code&gt;html&lt;/code&gt;/&lt;code&gt;body&lt;/code&gt; and, unless told
otherwise, descendants will inherit that font:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;body&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-family&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;system-ui&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So far, so good!&lt;/p&gt;

&lt;p&gt;The confusion tends to arrive when we introduce a web or custom font on a child
element, e.g.:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;h1&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-family&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Open Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At a glance, this can feel perfectly sensible. The page should use &lt;code&gt;system-ui,
sans-serif&lt;/code&gt;; the heading uses &lt;code&gt;"Open Sans"&lt;/code&gt;; and while the web font is loading,
the browser will presumably just fall back to the parent’s stack—&lt;code&gt;system-ui,
sans-serif&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Unfortunately, that isn’t the case.&lt;/p&gt;

&lt;h2 id="font-family-fallbacks-are-self-contained"&gt;&lt;code&gt;font-family&lt;/code&gt; Fallbacks Are Self-Contained&lt;/h2&gt;

&lt;p&gt;Once you declare a &lt;code&gt;font-family&lt;/code&gt; on an element, that declaration stands on its
own. The element does not say: &lt;q&gt;I would like &lt;code&gt;"Open Sans"&lt;/code&gt;, and if that is
unavailable right now, please work your way back up the DOM and inherit whatever
fallbacks the nearest ancestor might have.&lt;/q&gt;&lt;/p&gt;

&lt;p&gt;Instead, it says: &lt;q&gt;My &lt;code&gt;font-family&lt;/code&gt; is &lt;code&gt;"Open Sans"&lt;/code&gt;.&lt;/q&gt; And that’s all it
says.&lt;/p&gt;

&lt;p&gt;And if the browser does not yet have &lt;code&gt;"Open Sans"&lt;/code&gt; available (yet), it resolves
fallback from &lt;em&gt;that declaration&lt;/em&gt;, not from the parent’s.&lt;/p&gt;

&lt;p&gt;Put another way:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;h1&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-family&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Open Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;/* « The fallback happens inside here… */&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/**
 * …not here.
 */&lt;/span&gt;
&lt;span&gt;body&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-family&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;system-ui&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id="why-you-get-a-flash-of-times"&gt;Why You Get a Flash of Times&lt;/h2&gt;

&lt;p&gt;If the current element’s &lt;code&gt;font-family&lt;/code&gt; declaration contains only one value, and
that value is not currently available, the browser falls back to its default
&lt;strong&gt;for that element&lt;/strong&gt;, and not to an inheritable &lt;code&gt;font-family&lt;/code&gt; from somewhere
higher up. For most browsers in their default state, that fallback is likely
&lt;em&gt;Times&lt;/em&gt; or &lt;em&gt;Times New Roman&lt;/em&gt;. That is why you so often see a brief flash of
Times New Roman where you were expecting something much more sympathetic or
appropriate.&lt;/p&gt;

&lt;p&gt;The browser is not &lt;em&gt;forgetting&lt;/em&gt; the parent’s font stack; it’s obeying the
child’s declaration exactly as written, then exhausting the options available in
that declaration, and then falling back to the browser default.&lt;/p&gt;

&lt;h2 id="the-fix-is-simple"&gt;The Fix Is Simple&lt;/h2&gt;

&lt;p&gt;Whenever you specify a &lt;code&gt;font-family&lt;/code&gt;, specify a &lt;strong&gt;complete stack&lt;/strong&gt;. I’m looking
at a client’s site right now and I can see this right at the very top of their
CSS:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;:root&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;--hero-hero&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-medium-subtle&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-small-subtle&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-3-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-2-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At the very least, all of these simply need to read:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;:root&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;--hero-hero&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-medium-subtle&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-small-subtle&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-3-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-2-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Remember, any time you declare a &lt;code&gt;font-family&lt;/code&gt;, declare the whole thing. Even if
that is just a broad
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/generic-family"&gt;&lt;code&gt;&amp;lt;generic-family&amp;gt;&lt;/code&gt;&lt;/a&gt;
And while this is the bare minimum, at least sans-serif web fonts will actually fall
back to sans.&lt;/p&gt;

&lt;p&gt;To do a much more thorough job, you can simply &lt;a href="https://csswizardry.com/contact/"&gt;hire me&lt;/a&gt; to run my
&lt;cite&gt;Web Performance for Design Systems&lt;/cite&gt; workshop.&lt;/p&gt;

&lt;h2 id="why-this-matters"&gt;Why This Matters&lt;/h2&gt;

&lt;p&gt;At its most simple, this is a trivial visual issue: a nascent sans heading
briefly rendered in serif just looks wrong.&lt;/p&gt;

&lt;p&gt;At the other end of the spectrum, it can have real knock-on effects on Core Web
Vitals: if the fallback face is excessively different in width, height, or
overall proportions, the eventual swap to the web font can have an impact on
your CLS scores.&lt;/p&gt;

&lt;h2 id="closing-thoughts"&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;If a &lt;code&gt;font-family&lt;/code&gt; matters enough to override, it matters enough to define
properly. This is one of those small details that feels too small to matter
right up until you notice it everywhere.&lt;/p&gt;

&lt;p&gt;My client has had complaints of noticeable layout shifts while migrating to
a new design system, and at the size and scale they’re working at, they were
really, really struggling to pin it down. It only took me a few minutes because
&lt;em&gt;it’s easy when you know the answer&lt;/em&gt;. That’s exactly why you &lt;a href="https://csswizardry.com/consultancy/"&gt;hire
consultants&lt;/a&gt;.&lt;/p&gt;


      &lt;/div&gt;

        

        

        

        

        &lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Harry Roberts</name>
        </author>
        <media:content medium="image" url="https://cdn.requestmetrics.com/agent/current/rm.js"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://csswizardry.com/feed.xml</id>
            <title type="html">csswizardry.com</title>
            <link href="https://csswizardry.com" rel="alternate" type="text/html"/>
            <updated>2026-04-16T10:53:17Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19d7d22e1ed:2f12e0:29a1775c</id>
        <title type="html">Under the hood of MDN's new frontend</title>
        <published>2026-04-11T15:22:11Z</published>
        <updated>2026-04-11T15:22:17Z</updated>
        <link href="https://developer.mozilla.org/en-US/blog/mdn-front-end-deep-dive/" rel="alternate" type="text/html"/>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;
            
    &lt;section&gt;&lt;p&gt;Last year, we &lt;a href="https://developer.mozilla.org/en-US/blog/launching-new-front-end/"&gt;launched a new frontend for MDN&lt;/a&gt;.
The most noticeable changes were adjustments to our styles; we simplified and unified the MDN design across all of our pages.
In truth, the biggest changes were not reader-visible, but rather in the overhauled code that powers our frontend.
This post describes what we've done, the technologies we chose, and why we did it at all.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;To fully understand the changes we made to MDN's frontend, I should provide some context on how MDN content is assembled into the website you all know and love.
MDN's architecture is probably worthy of its own blog post, but to simplify for the sake of this post, pages are published to the site via these major steps:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;The documentation is written and maintained in Markdown, across a couple of git repositories, by our fantastic team of technical writers, partners, and invited experts, alongside our enormous community of contributors and translators.&lt;/li&gt;
&lt;li&gt;A build tool ingests these Markdown files, converts them into HTML, and saves them as a set of JSON files with supplemental metadata about each page.&lt;/li&gt;
&lt;li&gt;Our frontend traverses these JSON files and compiles fully-featured pages, complete with browser compatibility tables, l10n support, navigation menus, and so on - in a step we name (or perhaps misname) &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/SSR"&gt;server-side rendering&lt;/a&gt; (SSR).&lt;/li&gt;
&lt;li&gt;At this point, the resulting HTML, CSS, and JavaScript files are uploaded to cloud buckets and delivered to our readers globally.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;&lt;img src="https://developer.mozilla.org/en-US/blog/mdn-front-end-deep-dive/architecture.svg" alt="A flow diagram showing MDN's build pipeline in four steps: 1. Markdown from content and translated-content repositories, written by writers, partners, and community; 2. A build tool converting Markdown to HTML and JSON metadata; 3. Frontend SSR compiling JSON into full pages with compat tables, l10n, navigation, Web Components, and Server Components; 4. Cloud delivery of HTML, CSS, and JS via CDN to readers globally."&gt;&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;Rebuilding our frontend had been a long time coming because of how tricky it was to work on MDN's UI.
Our previous frontend (called &lt;strong&gt;yari&lt;/strong&gt;) was a React app that, unfortunately, had accumulated quite a lot of technical debt.
Maintenance wasn't exactly impossible, but was certainly painful to undertake.
Whenever we fixed issues or added new site functionality, we inevitably ended up piling on more technical debt.
But how did we get there?&lt;/p&gt;
&lt;p&gt;The React app had started life as a "Create React App", but a number of the built-in defaults didn't work for us.
Of course, this led to a series of workarounds, and we eventually had to &lt;a href="https://create-react-app.dev/docs/available-scripts/#npm-run-eject" target="_blank"&gt;"eject" the configuration&lt;/a&gt;. We ended up with an extremely complicated Webpack config as well as some very hacky build scripts.&lt;/p&gt;
&lt;p&gt;On the CSS side as well, things were starting to get out of control.
We used &lt;a href="https://sass-lang.com/" target="_blank"&gt;Sass&lt;/a&gt; extensively, then added modern CSS features like CSS variables, which meant we had a bizarre mix of both idioms spread across our files.&lt;/p&gt;
&lt;p&gt;The CSS was also incredibly entangled, with poor or nonexistent scoping.
When we made a change in one UI component, we'd frequently spot unintended changes in others.
These issues, and a lack of build tools to split up the CSS, meant we had to ship a large render-blocking CSS blob to our users, complete with styles for components they might never load.&lt;/p&gt;
&lt;p&gt;But by far, the biggest issue was that our React app was merely a wrapper around our static content.
To make the React app aware of the HTML content that our build tool generated would have required expensive reparsing of the HTML and an extraordinary amount of logic which we'd have to ship to users in our client-side JavaScript.
We didn't want to do this, so the React app boundary essentially ended where our documentation began – we used React's &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; to insert the content.&lt;/p&gt;
&lt;p&gt;Our content is &lt;em&gt;mostly&lt;/em&gt; static (prose and code examples), but there were a number of places within this static content where we needed to add interactivity (think things like the "Copy" button on code blocks).
For these interactive parts, we ended up using regular DOM APIs, which wasn't very elegant, particularly when the rest of the site was written in React.
We couldn't use &lt;a href="https://react.dev/learn/writing-markup-with-jsx" target="_blank"&gt;JSX&lt;/a&gt; (React's HTML-like syntax), which limited the maintainability of more complex pieces of interactivity, and we occasionally faced the worst-case scenario of maintaining duplicate implementations - one using React and another using DOM APIs.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;As a possible solution to this problem, in 2024, we started experimenting with &lt;a href="https://lit.dev/" target="_blank"&gt;Lit&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components"&gt;web components&lt;/a&gt; to see whether they could improve the developer experience when working on this kind of interactivity within content.
Our first proper prototype, and eventual production implementation, came out of our work on the &lt;a href="https://developer.mozilla.org/en-US/curriculum/"&gt;MDN Curriculum&lt;/a&gt; when we partnered with &lt;a href="https://scrimba.com" target="_blank"&gt;Scrimba&lt;/a&gt;.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;Scrimba has a feature called "Scrims" - an interactive learning environment we embed on MDN via an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;. Scrims let learners watch a short coding tutorial and then edit the code themselves, all within the same view — think of them as interactive screencasts.&lt;/p&gt;
&lt;p&gt;On our pages, we didn't want to send any user data to Scrimba until a user chose to interact with their content; so we didn't load the &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; until after a user clicks to open it.
We also wanted to be able to expand the Scrim to fullscreen, without a user leaving MDN, so we used the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog"&gt;&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;&lt;/a&gt; element.&lt;/p&gt;
&lt;p&gt;We figured that building a web component would allow us to use a custom element to insert these Scrims directly into our content, thereby skipping a number of rendering steps and avoiding the tricky-to-maintain DOM API implementation.&lt;/p&gt;
&lt;p&gt;Our component starts life by extending &lt;code&gt;LitElement&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;Within that, we need to define some state.
In Lit, we can do this through a static properties attribute:&lt;/p&gt;

&lt;p&gt;And we set defaults in our class constructor:&lt;/p&gt;

&lt;p&gt;We want to manipulate the URL which has been provided as an attribute to our custom element.
Lit provides us with &lt;a href="https://lit.dev/docs/components/lifecycle/" target="_blank"&gt;lifecycle methods&lt;/a&gt; to do this. We want to compute a value once we know an update to the component will be rendered:&lt;/p&gt;

&lt;p&gt;We can then use this state to render our component:&lt;/p&gt;

&lt;p&gt;Lit's &lt;code&gt;html&lt;/code&gt; template literal is just as convenient as JSX in allowing us to write HTML-ish syntax in JavaScript.
The huge advantage over JSX is it doesn't require any compilation to use: it's native JavaScript.&lt;/p&gt;
&lt;p&gt;I say "HTML-ish" because you'll notice a few annotations before certain attributes in the template above, namely &lt;code&gt;@close&lt;/code&gt; and &lt;code&gt;@click&lt;/code&gt;.
This is Lit syntax that allows us to bind event listeners to these elements: the close event on the &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; and click events on a couple of buttons.
We define these in the class too:&lt;/p&gt;

&lt;p&gt;When a user clicks to open the Scrim, the &lt;code&gt;#open&lt;/code&gt; method fires, which updates the values of &lt;code&gt;_scrimLoaded&lt;/code&gt; and &lt;code&gt;_fullscreen&lt;/code&gt;.
Lit notices the changes to these properties, because we'd defined them in &lt;code&gt;static properties&lt;/code&gt;, and automatically re-renders the component, loading the &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; and the Scrim inside.&lt;/p&gt;
&lt;p&gt;I've simplified the component a little for brevity, you can see the &lt;a href="https://github.com/mdn/fred/blob/main/components/scrim-inline/element.js" target="_blank"&gt;&lt;code&gt;MDNScrimInline&lt;/code&gt; source on GitHub&lt;/a&gt;.
There's a number of additions in there like telemetry, and a dynamically-rendered thumbnail (it was fewer bytes and a simpler implementation than pre-rendering a bunch of images).
As you can imagine, this was very straightforward to develop, thanks to the convenience functions we get with Lit; this would've been a massive headache to implement directly with traditional DOM APIs.&lt;/p&gt;
&lt;p&gt;In many ways, I found that the implementation in Lit was simpler than in React: you may notice the state we're dealing with isn't particularly complex, and doesn't require a complex component architecture to reflect it.
But more importantly, it gives us that custom element we can insert into our curriculum content wherever we need to add a Scrim:&lt;/p&gt;

  &lt;/section&gt;&lt;section&gt;&lt;p&gt;The Scrimba implementation was a good introduction for the team to writing a small component, but what about something more complex?
Interactive examples are the components that appear under "Try it" sections at the top of many CSS, JavaScript, and HTML pages.&lt;/p&gt;
&lt;p&gt;Improving the infrastructure for these had been on the engineering team backlog for a while; they were difficult for our technical writers and community to maintain and author.
The existing implementation was split across four git repositories, and authoring or debugging an example could require synchronizing changes across them all.
Worse still, examples had to be written in isolation from the content they'd be included in, so it wasn't possible to show a live preview containing both changes to an example and the content of the MDN page it would be included on.&lt;/p&gt;
&lt;p&gt;This complexity came about for good reasons: these interactive examples were too complex to easily engineer and maintain with DOM APIs directly.
Instead, we had a separate build system and examples repository which rendered these examples into separate HTML pages which we could load in an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; directly.&lt;/p&gt;
&lt;p&gt;We wanted to simplify this architecture, to make writing interactive examples easier for our authors, so again: we reached for Lit to build a web component we could include directly in our content.
This was a much more technically-complex implementation than Scrims.
Firstly, we needed a number of templates for the different ways interactive examples are displayed:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;A code editor and console for JavaScript examples, with &lt;a href="https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Memory/load"&gt;the addition of tabs for WASM&lt;/a&gt; examples.&lt;/li&gt;
&lt;li&gt;A tabbed code editor with rendered output for HTML examples (usually &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/p#try_it"&gt;tabs for HTML and CSS&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;A table of code editors that can be selected with rendered output for CSS examples (see the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/background-clip#try_it"&gt;CSS &lt;code&gt;background-clip&lt;/code&gt; property&lt;/a&gt;, for example).&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Secondly, we needed a way to render the examples, and the edits users made to them.
We had already written that logic to create our interactive &lt;a href="https://developer.mozilla.org/en-US/blog/introducing-the-mdn-playground/"&gt;Playground&lt;/a&gt;, but it was in React: so we needed to port that too.&lt;/p&gt;
&lt;p&gt;So we set about doing all that.
What made things a lot simpler was that &lt;a href="https://lit.dev/docs/frameworks/react/" target="_blank"&gt;Lit's React integration&lt;/a&gt; allowed us to render these web components in our existing React app.
So we could port the elements of the Playground we needed piece-by-piece to web components, without having to port the entire thing all at once, and without having to maintain dual implementations.&lt;/p&gt;
&lt;p&gt;At a high level, we split our single entangled Playground React component into a series of custom elements:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;&amp;lt;play-editor&amp;gt;&lt;/code&gt;: A &lt;a href="https://codemirror.net/" target="_blank"&gt;CodeMirror&lt;/a&gt;-powered editor.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;play-console&amp;gt;&lt;/code&gt;: An element to format and render console messages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;play-runner&amp;gt;&lt;/code&gt;: An element responsible for rendering the current state of each editor.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;play-controller&amp;gt;&lt;/code&gt;: An element responsible for passing events and state between each of the above elements.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This made the logic in the Playground simpler and decoupled, which allowed us to reuse these elements in the &lt;code&gt;&amp;lt;interactive-example&amp;gt;&lt;/code&gt; element we created.
This included logic to search the page for &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt; elements the interactive example component should ingest, sending their contents to a &lt;code&gt;&amp;lt;play-controller&amp;gt;&lt;/code&gt;, and working out which of the above templates it needed to render: using some combination of the &lt;code&gt;&amp;lt;play-*&amp;gt;&lt;/code&gt; elements to do this.&lt;/p&gt;
&lt;p&gt;This meant that authors could now add a macro to content - which renders our &lt;code&gt;&amp;lt;interactive-example&amp;gt;&lt;/code&gt; custom element behind the scenes - followed by the code blocks the example should use:&lt;/p&gt;

&lt;p&gt;You can see the full source for this example on the &lt;a href="https://github.com/mdn/content/blob/616b1da6696a833451891ad8c767ff15474b08f7/files/en-us/web/css/background-repeat/index.md?plain=1#L11-L50" target="_blank"&gt;CSS background-repeat page GitHub&lt;/a&gt;.
We're not quite sure where we stand on putting custom elements directly in our non-curriculum Markdown content, which could make our architecture even simpler; that's a discussion for another day.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;So this is all well and good, web components seem pretty cool and solve some of our problems around adding interactivity within our static content. But wasn't this blog post supposed to be about how we rewrote our entire frontend stack: what happened to that? To answer that, let's get into another problem we had with our old frontend:&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;I've mentioned our problem with our React app being a "wrapper" and not being able to interact with our content.
The fundamental problem is that (at least classically) React apps are Single Page Applications (SPAs), which you figure out how to render on the server, then attempt to figure out how to avoid shipping an absolutely enormous JavaScript bundle to your users.&lt;/p&gt;
&lt;p&gt;That last part is necessary. That's because everything you render in an SPA, even if it could be rendered statically on a server or in a compilation step, has to be shipped in your client-side JavaScript bundle and re-rendered in the client – just to verify nothing has changed.
The React documentation itself summarises it better than I could:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This pattern means users need to download and parse an additional 75K (gzipped) of libraries, and wait for a second request to fetch the data after the page loads, just to render static content that will not change for the lifetime of the page. &lt;br&gt;&lt;a href="https://react.dev/reference/rsc/server-components" target="_blank"&gt;Server Components&lt;/a&gt; on react.dev&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's a quote from the documentation for React Server Components (RSC): so the project recognizes that this is a problem, and is working hard on solving it.
Unfortunately, using RSC effectively requires using a framework that we aren't already using.
Migrating to that would require rewriting a lot of the frontend anyway.&lt;/p&gt;
&lt;p&gt;So since a large rewrite was required to address this fundamental issue, we could also re-evaluate what kind of a site MDN is, and how complex it needed to be.
Really, MDN isn't a particularly complex site, at least from a "things that require interactivity" standpoint.
The vast majority of content on an MDN documentation page is HTML and CSS: we don't need a complex app powering the majority of the site.
We essentially have islands of interactivity, which could easily all be implemented as web components.&lt;/p&gt;
&lt;p&gt;And if we're implementing all our functionality in isolated web components, it doesn't really matter how they're assembled: we just need to template HTML together, and that can happen multiple times, in multiple places in our overall build system.
There's no higher-level "app" that needs to understand the state of the entire page, so there never could be a "wrapper" problem – our markdown to HTML build tool is just as first class a citizen as whatever templating we need to do in our frontend.&lt;/p&gt;
&lt;p&gt;This approach solved all three problems at once: there's no SPA shipping redundant JavaScript to re-render static content, there's no "wrapper" that can't reach into our documentation HTML, and each piece of interactivity is a self-contained web component that only loads when it's needed. What remained was deciding how to do the static templating that assembles everything else.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;We considered using a dedicated templating language, such as EJS, to do templating in our frontend, but realized there's a lot of benefits to a component-based architecture.
While doing static HTML templating on the server avoids shipping logic to users in a client-side JavaScript bundle, this HTML still requires styling.
And as you may recall, the CSS in our old frontend was a mess, and we wanted to avoid shipping unnecessary CSS if it wasn't necessary to render the current page.&lt;/p&gt;
&lt;p&gt;We first built our own concept of Server Components, using Lit's HTML template literal, which we were already comfortable with.
Here's an example of our top navigation bar component:&lt;/p&gt;

&lt;p&gt;You'll see the logic is handled in a &lt;code&gt;render&lt;/code&gt; method, much like it would be in a Lit component.
We don't need any lifecycle methods because this only ever runs once.
This component can render other server components like &lt;code&gt;Logo&lt;/code&gt; and &lt;code&gt;Menu&lt;/code&gt;, as well as web components like &lt;code&gt;&amp;lt;mdn-search-button&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;mdn-search-modal&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We render this to HTML in NodeJS, using a convenient function Lit provides.
This also renders these Lit web components to &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM"&gt;Declarative Shadow DOM&lt;/a&gt; so, in compatible browsers, the Shadow DOM and CSS of our custom elements is rendered before the JavaScript gets loaded.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;As I mentioned before, one big problem with an SPA-approach to building websites is everything necessary to render the page needs to be included in the client-side JavaScript bundle.
Another similar problem is that it's very easy, and therefore very common, to ship a whole load of code &lt;em&gt;not&lt;/em&gt; necessary for rendering the current page, and only necessary for rendering other pages, in one huge client-side bundle.
We fell into this trap with our old frontend, both with our JavaScript and our CSS.&lt;/p&gt;
&lt;p&gt;Over time we did partition off certain routes into separate chunks, but this was only possible with our JavaScript. Our CSS was too entangled to do this, and our build tool wasn't configured to do it either.&lt;/p&gt;
&lt;p&gt;And, it was only possible to do this &lt;em&gt;per route&lt;/em&gt;, not for components within the same page. If there was a chance a certain route might load a component, it needed to be included in the bundle if it was to be server-side rendered, even if it wasn't, and that JavaScript was never executed client side.&lt;/p&gt;
&lt;p&gt;We wanted to avoid all this in our new frontend: only loading the most minimal CSS and JavaScript bundles required to render the page, and making it interactive; and I wanted to achieve this on an architectural level, so it was nearly impossible to not do. We achieved this in a few ways, but the key to unlocking them all was a flat name-based component structure.&lt;/p&gt;
&lt;p&gt;Every component lives in a flat hierarchy under the &lt;code&gt;./components/&lt;/code&gt; directory, with the following file names reserved for certain pieces of the component:&lt;/p&gt;
&lt;pre&gt;components/example-component
├── element.css
├── element.js
├── global.css
├── server.css
└── server.js
&lt;/pre&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;element.js&lt;/code&gt; - A web component, exporting a &lt;code&gt;MDNExampleComponent&lt;/code&gt; class and defining a &lt;code&gt;&amp;lt;mdn-example-component&amp;gt;&lt;/code&gt; element.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server.js&lt;/code&gt; - A server component, extending &lt;code&gt;ServerComponent&lt;/code&gt; from &lt;a href="https://github.com/mdn/fred/blob/main/components/server/index.js" target="_blank"&gt;&lt;code&gt;components/server/index.js&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server.css&lt;/code&gt; - CSS for a server component that will be automatically loaded for this component from the server.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.css&lt;/code&gt; - CSS for the component that gets loaded everywhere all the time.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;We enforce parts of this via linting, and some of this by throwing errors if you don't adhere to the naming requirements.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;Because we know where each web component lives based on name, we can do some clever things.
When our page loads, we run logic like this client-side:&lt;/p&gt;

&lt;p&gt;This results in lazy-loading every custom element present in the DOM at load time, entirely async and in parallel.
The advantages here are numerous:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Engineers don't have to remember to import web components in server components; they can use them as if they are normal HTML elements.&lt;/li&gt;
&lt;li&gt;We can add custom elements to our content markdown (either directly or through a macro), without having to wire up an export elsewhere.&lt;/li&gt;
&lt;li&gt;We only ever load the JavaScript for each web component if it's present on the page, automatically.
It's not necessary for engineers to think about whether their new component increases the bundle size - if it's not present on the page the user is viewing, that code won't end up being loaded by the users' browser.&lt;/li&gt;
&lt;li&gt;Changes to one component should only have minimal impact on the rest of the bundle: a bugfix in one component will require that component's JavaScript to be reloaded, but other components should be cached by the browser and will become interactive almost immediately, as they load in parallel asynchronously.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;We also automatically load every web component in our SSR bundle, where Lit renders them into a Declarative Shadow DOM (unless the component opts out because it doesn't make sense to render them on the server).
This helps ensure we don't have layout shifts when the JavaScript loads.
The result is that the slight delay to interactivity when we load its JavaScript after initial render is imperceptible because the component is already on the page, just not interactive yet.&lt;/p&gt;
&lt;p&gt;At least, not entirely interactive: this architecture is flexible enough for us to be very clever with certain components.
One of these is &lt;code&gt;&amp;lt;mdn-dropdown&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is a component which, as the name might suggest, implements a dropdown.
The render method is rather simple:&lt;/p&gt;

&lt;p&gt;We render two slots, which can be used like so:&lt;/p&gt;

&lt;p&gt;Since we use slots here, &lt;code&gt;&amp;lt;mdn-dropdown&amp;gt;&lt;/code&gt;'s shadow DOM is almost irrelevant, and any children of it can be styled entirely as normal: the element only adds interactivity.
It &lt;em&gt;does&lt;/em&gt; have some styles attached, but those aren't for styling: they're for interactivity too.
See, we also have a lifecycle method:&lt;/p&gt;

&lt;p&gt;And define our loaded property like so:&lt;/p&gt;

&lt;p&gt;If you trace the logic through, you'll see that the dropdown slot isn't hidden by default, and therefore, is visible when we render to DSD on the server.
And once the component is &lt;code&gt;loaded=true&lt;/code&gt;, we reflect that attribute into the DOM, so it appears like:&lt;/p&gt;

&lt;p&gt;The reason we want this is because we also attach the following CSS to the element:&lt;/p&gt;

&lt;p&gt;The logic here is a little hard to parse - and I say that as the person who wrote it - but it effectively translates to:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;If JavaScript for the &lt;code&gt;mdn-dropdown&lt;/code&gt; has loaded, do no styling.&lt;/li&gt;
&lt;li&gt;If the JavaScript for the &lt;code&gt;mdn-dropdown&lt;/code&gt; hasn't loaded, then:
&lt;ul&gt;&lt;li&gt;If the focus is outside the element, hide the dropdown slot.&lt;/li&gt;
&lt;li&gt;If the focus is within the element, show the dropdown slot.
And what does this mean? Well, we have a CSS native dropdown as soon as the page is rendered, which progressively enhances to a JavaScript dropdown, once that code has loaded; other engineers need not know any of this is going on, just that the dropdown component works.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This is hugely important in our top navigation menu, where most of our links are behind dropdowns: those are completely usable as soon as they're rendered to the page.
In other components, such as in our theme switcher, while choosing between themes isn't possible until its JavaScript loads, the dropdown being interactive already gives us a few seconds longer load time for that JavaScript before the user clicks on anything requiring it.&lt;/p&gt;
&lt;h4 id="what_if_we_dont_have_dsd"&gt;What if we don't have DSD&lt;/h4&gt;
&lt;p&gt;Now, Declarative Shadow DOM isn't widely available yet, so we have to ensure things also work on slightly older browsers.
This is where the &lt;code&gt;global.css&lt;/code&gt; file comes in: any CSS written in one of these is included on all pages, all the time.&lt;/p&gt;
&lt;p&gt;This is obviously necessary for setting things like global CSS variables, global reset styles, and the like.
But for components, when DSD isn't available, before its JavaScript has loaded, they'll appear to the browser as an empty inline element with no styling attached.
This isn't always optimal, and can cause layout shifts when the JavaScript gets loaded, so we set global styles for certain elements, for example, for our button component:&lt;/p&gt;

&lt;p&gt;This is just enough to ensure buttons don't shift the layout before being loaded.
There's a small optimisation to be had by only loading this style when an &lt;code&gt;mdn-button&lt;/code&gt; is present on the page, rather than on every page: but it's so minimal it's probably not worth the added complexity.
It's also important for our aforementioned dropdown component: we still want this to be interactive if DSD hasn't loaded, so we include a similar style in a &lt;code&gt;global.css&lt;/code&gt; file.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;For server components, the considerations are a little different. The JavaScript &lt;em&gt;itself&lt;/em&gt; doesn't need to be lazy loaded or cut down, since it's only being used to SSR our HTML. But what does require careful loading is the CSS used in each server component. We only want to load this if the component gets rendered to the page. But how do we know this?&lt;/p&gt;
&lt;p&gt;Our server components extend our &lt;code&gt;ServerComponent&lt;/code&gt; class, so we place some tracking logic in its static render method, which runs before and after instantiating each server component:&lt;/p&gt;

&lt;p&gt;This gives us a &lt;code&gt;Set&lt;/code&gt;, &lt;code&gt;componentsUsed&lt;/code&gt;, which only contains the components that rendered anything. We then use this in our &lt;code&gt;OuterLayout&lt;/code&gt; component:&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;compilationStats&lt;/code&gt; object here comes from our build tool, Rspack (more on that later), and this code block gives us a list of &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags to include in our &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; containing only the CSS that's necessary to render the page. We also load a CSS file with all the &lt;code&gt;global.css&lt;/code&gt; files mentioned before, bundled into one.&lt;/p&gt;
&lt;p&gt;Again, this is a simplified version of our &lt;a href="https://github.com/mdn/fred/blob/main/components/server/index.js" target="_blank"&gt;&lt;code&gt;ServerComponent&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/mdn/fred/blob/main/components/outer-layout/server.js" target="_blank"&gt;&lt;code&gt;OuterLayout&lt;/code&gt;&lt;/a&gt; classes, which you can see in their entirety in our repository if you're interested.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;You'll note that what I've suggested here results in a number of quite small CSS and JavaScript files being loaded on the page. This goes against the classical wisdom that there's an optimal bundle size where you combine multiple assets into one to reduce the number of round trips a browser needs to make to load them all.&lt;/p&gt;
&lt;p&gt;I can't claim to be a performance expert here, but &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/HTTP_2"&gt;HTTP/2&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/HTTP_3"&gt;HTTP/3&lt;/a&gt; do a lot to change that wisdom. As we can now download assets in parallel, and reuse connections, multiple small assets don't have the overhead they did before, and can be advantageous, particularly given how our web components load.&lt;/p&gt;
&lt;p&gt;As I described before, since we load our web components asynchronously and independently after the page has rendered, it's faster to fire these down the wire component by component - so the browser can act on adding interactivity as soon as the code has loaded - rather than all in one blob where the browser has to parse the code for multiple components, perhaps to only add interactivity for one.&lt;/p&gt;
&lt;p&gt;Once you throw caching into the mix, things get even faster, and this extends to our CSS too: an update to one component in many cases won't touch the bundled code of the others. So for a user re-visiting MDN, they'll get interactivity for components that have been cached near-instantly, and for any that have changed - or for any server component CSS that has changed - they'll only be waiting for that changed component to download.&lt;/p&gt;
&lt;p&gt;Benchmarking these things is always required, and we could always do more, but what we've done so far showed that bundling things together was only as good or slower with a cold cache. We do also have a few levers in our build config to pull to easily bundle smaller components together if future benchmarking shows that that would be better.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;We're using quite a number of more modern web technologies here, and we needed an easy way to determine if we could use something, and if we could do without polyfills or progressive enhancement. Luckily enough, we've spent the last few years working on the Baseline project with a cross-vendor range of partners in the &lt;a href="https://www.w3.org/community/webdx/" target="_blank"&gt;WebDX group&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This gave us a fantastically easy way to determine whether to use an API or not. The advice I gave the other engineers was: if it's "Baseline Widely Available", just use it; if it's "Baseline Newly Available", come talk to me first, and we'll figure out a polyfill or if it can be used as a progressive enhancement; and if it's "Baseline Limited Availability" or you need to do something there's no API for yet, think some more about if you really need to do it, then come talk to me.&lt;/p&gt;
&lt;p&gt;We ended up using a range of technologies, across all these statuses:&lt;/p&gt;
&lt;p&gt;Things like Custom Elements and Shadow DOM have been supported cross-browser for a surprisingly long time these days, and are solidly Baseline Widely Available.&lt;/p&gt;
&lt;p&gt;Declarative Shadow DOM, as I mentioned earlier, is supported cross browser but hasn't been in the web platform long enough to be Widely Available, so we use it as a progressive enhancement, with fallbacks in place for older browsers which don't support it yet, like our use of &lt;code&gt;global.css&lt;/code&gt; stylesheets.
There's also a few things we wanted which are at the very bleeding edge: one of those was extending &lt;code&gt;light-dark&lt;/code&gt; to images, where we used PostCSS to define a custom mixin, allowing us to use syntax like this:&lt;/p&gt;

&lt;p&gt;Using Baseline allows us to build confidently - knowing the vast majority of users can use a feature once it's reached Widely Available - and also keep our set of polyfills (and their overhead) small as we automatically remove them when features move from Newly Available to Widely Available.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;I've saved what I think our best improvement is until last: but I'm a little biased as an engineer working on MDN. Our old frontend development environment pained me every time I had to use it.&lt;/p&gt;
&lt;p&gt;The big problems were:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;It was slow: the default start command took about two minutes to present you with a functional locally running SPA, not including the time to download npm packages and so on.&lt;/li&gt;
&lt;li&gt;It was complex: there were an enormous number of commands in our &lt;code&gt;package.json&lt;/code&gt;, some of which gave you a development environment faster by skipping elements of the build, but that required an intricate knowledge of what exactly these complex commands were doing to know if you needed them or not.&lt;/li&gt;
&lt;li&gt;It didn't reliably restart: frequently changes, even simple ones like adding a new image, would require restarting the development server - and waiting another two minutes - to see the change.&lt;/li&gt;
&lt;li&gt;By default, we only rendered the SPA, with no SSR: that required running a separate command, which only created a production bundle and took even longer to run, which made debugging certain issues with SSR exceptionally difficult to do.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;We were very keen to improve this - obviously for our own sake - but also to make the contribution process easier, and we have: the new frontend takes 2 &lt;em&gt;seconds&lt;/em&gt; to start, and there's really only one command you need:&lt;/p&gt;

&lt;p&gt;An enormous amount of this speed comes from using &lt;a href="https://rspack.rs/" target="_blank"&gt;Rspack&lt;/a&gt; as our build tool.
Webpack was what the old frontend used, and to its credit is fantastically configurable - something we needed given the approaches we took with our architecture.
Rspack has a webpack-compatible API, but it's written in Rust and is incredibly fast.&lt;/p&gt;
&lt;p&gt;Though &lt;a href="https://github.com/mdn/fred/blob/main/rspack.config.js" target="_blank"&gt;our Rspack config&lt;/a&gt;, currently standing at 650 LOC isn't necessarily &lt;em&gt;simple&lt;/em&gt;, I would argue it's &lt;em&gt;straightforward&lt;/em&gt;. There's very little logic hidden away, magically happening in our build tool. This config does a lot for us, including all the bundling, polyfilling, mixins, and optimizations I described before.&lt;/p&gt;
&lt;p&gt;Our architecture is simple enough to rely on a single command that does almost everything. Unlike before, there aren't really separate things to build independently. There's no SPA that we can render without SSR, as server components are fundamental to our architecture. We don't need multiple commands because there's only one way our website is assembled.&lt;/p&gt;
&lt;p&gt;This gives us an environment far more similar to our production environment than before, with the main difference being whether we reload the page on every change, and reload and re-render our server components on each request. This means we almost never have to restart our development environment, unless we're making changes to our Rspack config itself or applying various production-level optimizations. We have another command to build a production build with those optimizations and without the dynamic loading, which we can easily run if we need.&lt;/p&gt;
&lt;p&gt;Developing in this new environment has been an absolute joy for me, and I'm so glad we managed to make these improvements.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;I think you'll agree that this blog post is long enough, and I hear the Slack pings coming in from our content team telling me to finish this post already, but I don't feel like I've even told you half the story of our new frontend architecture!&lt;/p&gt;
&lt;p&gt;If you're interested in learning more, or have any questions about anything we've done, please feel free to come chat with us in the &lt;a href="https://discord.com/channels/1009925603572600863/1170042997212184576" target="_blank"&gt;#platform channel&lt;/a&gt; on our Discord.&lt;/p&gt;
&lt;p&gt;If you spot any issues, please raise them in &lt;a href="https://github.com/mdn/fred/issues" target="_blank"&gt;the fred GitHub repository&lt;/a&gt;. And if anything there looks like something you'd like to fix, have a go and submit a PR.&lt;/p&gt;
&lt;p&gt;Building this new frontend was a complete pleasure: it's been a privilege to be able to use new web technologies to build &lt;em&gt;the&lt;/em&gt; website that documents them.&lt;/p&gt;
  &lt;/section&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://transcend-cdn.com/cm/d556c3a1-e57c-4bdf-a490-390a1aebf6dd/airgap.js"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://developer.mozilla.org/en-US/blog/rss.xml</id>
            <title type="html">developer.mozilla.org</title>
            <link href="https://developer.mozilla.org" rel="alternate" type="text/html"/>
            <updated>2026-04-11T15:22:17Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/c1+r4TpK8uaTr2UOrMQDK0/1GSCBZDCx6Gq/d2cjVgo=_19cf035cda2:e7ddef:7d8a2c4</id>
        <title type="html">Tech's empiricism problem</title>
        <published>2026-03-15T06:36:23Z</published>
        <updated>2026-03-16T06:12:02Z</updated>
        <link href="https://deadsimpletech.com/blog/tech_empiricism_problem" rel="alternate" type="text/html"/>
        <summary type="html">The tech industry has extreme difficulty integrating information that doesn't have its source in an overtly rationalist process. In practice, this means that we tend to think that if you can't give a logical chain of deductions that proves that something is the case, your information is worthless. The issue with this is that day-to-day, in the tech world and outside of it, the vast bulk of the information we use to make decisions isn't this kind of information.</summary>
        <content type="html">The tech industry has extreme difficulty integrating information that doesn't have its source in an overtly rationalist process. In practice, this means that we tend to think that if you can't give a logical chain of deductions that proves that something is the case, your information is worthless. The issue with this is that day-to-day, in the tech world and outside of it, the vast bulk of the information we use to make decisions isn't this kind of information.</content>
        <author>
            <name/>
        </author>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://deadsimpletech.com/rss</id>
            <title type="html">deadSimpleTech blog feed</title>
            <link href="https://deadsimpletech.com" rel="alternate" type="text/html"/>
            <updated>2026-03-16T06:12:02Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cc6dd7b70:3f8aa9:8275c42b</id>
        <title type="html">Nobody Gets Promoted for Simplicity</title>
        <published>2026-03-07T05:55:29Z</published>
        <updated>2026-03-07T05:55:34Z</updated>
        <link href="https://terriblesoftware.org/2026/03/03/nobody-gets-promoted-for-simplicity/" rel="alternate" type="text/html"/>
        <summary type="html">We reward complexity and ignore simplicity. In interviews, design reviews, and promotions. Here’s how to fix it.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
&lt;blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"&gt;
&lt;p class="wp-block-paragraph"&gt;&lt;em&gt;“Simplicity is a great virtue, but it requires hard work to achieve and education to appreciate. And to make matters worse, complexity sells better.”&lt;/em&gt; — Edsger Dijkstra&lt;/p&gt;
&lt;/blockquote&gt;



&lt;p class="wp-block-paragraph"&gt;I think there’s something quietly screwing up a lot of engineering teams. In interviews, in promotion packets, in design reviews: the engineer who overbuilds gets a compelling narrative, but the one who ships the simplest thing that works gets… nothing.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;This isn’t intentional, of course. Nobody sits down and says, &lt;em&gt;“let’s make sure the people who over-engineer things get promoted!”&lt;/em&gt; But that’s what can happen (and it has been, over and over again) when companies evaluate work incorrectly.&lt;/p&gt;



&lt;hr class="wp-block-separator has-alpha-channel-opacity"&gt;&lt;p class="wp-block-paragraph"&gt;Picture two engineers on the same team. Engineer A gets assigned a feature. She looks at the problem, considers a few options, and picks the simplest. A straightforward implementation, maybe 50 lines of code. Easy to read, easy to test, easy for the next person to pick up. It works. She ships it in a couple of days and moves on.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Engineer B gets a similar feature. He also looks at the problem, but he sees an opportunity to build something more “robust.” He introduces a new abstraction layer, creates a pub/sub system for communication between components, adds a configuration framework so the feature is “extensible” for future use cases. It takes three weeks. There are multiple PRs. Lots of excited emojis when he shares the document explaining all of this.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Now, promotion time comes around. Engineer B’s work practically writes itself into a promotion packet: &lt;em&gt;“Designed and implemented a scalable event-driven architecture, introduced a reusable abstraction layer adopted by multiple teams, and built a configuration framework enabling future extensibility.”&lt;/em&gt; That practically screams Staff+.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;But for Engineer A’s work, there’s almost nothing to say. &lt;em&gt;“Implemented feature X.”&lt;/em&gt; Three words. Her work was better. But it’s invisible because of how simple she made it look. You can’t write a compelling narrative about the thing you &lt;em&gt;didn’t&lt;/em&gt; build. &lt;strong&gt;Nobody gets promoted for the complexity they avoided&lt;/strong&gt;.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Complexity looks smart. Not because it is, but because our systems are set up to reward it. And the incentive problem doesn’t start at promotion time. It starts before you even get the job.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Think about interviews. You’re in a system design round, and you propose a simple solution. A single database, a straightforward API, maybe a caching layer. The interviewer is like: &lt;em&gt;“What about scalability? What if you have ten million users?”&lt;/em&gt; So you add services. You add queues. You add sharding. You draw more boxes on the whiteboard. The interviewer finally seems satisfied now.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;What you just learned is that complexity impresses people. The simple answer wasn’t wrong. It just wasn’t interesting enough. And you might carry that lesson with you into your career. To be fair, interviewers sometimes have good reasons to push on scale; they want to see how you think under pressure and whether you understand distributed systems. But when the takeaway for the candidate is “simple wasn’t enough,” something’s off.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;It also shows up in design reviews. An engineer proposes a clean, simple approach and gets hit with &lt;em&gt;“shouldn’t we future-proof this?”&lt;/em&gt; So they go back and add layers they don’t need yet, abstractions for problems that might never materialize, flexibility for requirements nobody has asked for. Not because the problem demanded it, but because the room expected it.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;I’ve &lt;a href="https://terriblesoftware.org/2025/05/28/duplication-is-not-the-enemy/"&gt;seen engineers&lt;/a&gt; (and have been one myself) create abstractions to avoid duplicating a few lines of code, only to end up with something far harder to understand and maintain than the duplication ever was. Every time, it felt like the right thing to do. The code looked more “professional.” More engineered. But the users didn’t get their feature any faster, and the next engineer to touch it had to spend half a day understanding the abstraction before they could make any changes.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Now, let me be clear: complexity is sometimes the right call. If you’re processing millions of transactions, you might need distributed systems. If you have 10 teams working on the same product, you probably need service boundaries. When the problem is complex, the solution (probably) should be too!&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;The issue isn’t complexity itself. It’s unearned complexity. There’s a difference between &lt;em&gt;“we’re hitting database limits and need to shard”&lt;/em&gt; and &lt;em&gt;“we might hit database limits in three years, so let’s shard now.”&lt;/em&gt;&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Some engineers understand this. And when you look at their code (and architecture), you think &lt;em&gt;“well, yeah, of course.”&lt;/em&gt; There’s no magic, no cleverness, nothing that makes you feel stupid for not understanding it. And that’s exactly the point.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;The &lt;a href="https://terriblesoftware.org/2025/11/25/what-actually-makes-you-senior/"&gt;actual path to seniority&lt;/a&gt; isn’t learning more tools and patterns, but learning when not to use them. &lt;strong&gt;Anyone can add complexity. It takes experience and confidence to leave it out&lt;/strong&gt;.&lt;/p&gt;



&lt;hr class="wp-block-separator has-alpha-channel-opacity"&gt;&lt;p class="wp-block-paragraph"&gt;So what do we actually do about this? Because saying “keep it simple” is easy. Changing incentive structures is harder.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;&lt;strong&gt;If you’re an engineer&lt;/strong&gt;, learn that simplicity needs to be made visible. The work doesn’t speak for itself; not because it’s not good, but because most systems aren’t designed to hear it.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Start with how you talk about your own work. “Implemented feature X” doesn’t mean much. But &lt;em&gt;“evaluated three approaches including an event-driven architecture and a custom abstraction layer, determined that a straightforward implementation met all current and projected requirements, and shipped in two days with zero incidents over six months”&lt;/em&gt;, that’s the same simple work, just described in a way that captures the judgment behind it. The decision &lt;em&gt;not&lt;/em&gt; to build something is a decision, an important one! Document it accordingly.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;In design reviews, when someone asks “shouldn’t we future-proof this?”, don’t just cave and go add layers. Try: &lt;em&gt;“Here’s what it would take to add that later if we need it, and here’s what it costs us to add it now. I think we wait.”&lt;/em&gt; You’re not pushing back, but showing you’ve done your homework. You considered the complexity and chose not to take it on.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;And yes, bring this up with your manager. Something like: &lt;em&gt;“I want to make sure the way I document my work reflects the decisions I’m making, not just the code I’m writing. Can we talk about how to frame that for my next review?”&lt;/em&gt; Most managers will appreciate this because you’re making their job easier. You’re giving them language they can use to advocate for you.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Now, if you do all of this and your team still only promotes the person who builds the most elaborate system… that’s useful information too. It tells you something about where you work. Some cultures genuinely value simplicity. Others say they do, but reward the opposite. If you’re in the second kind, you can either play the game or find a place where good judgment is actually recognized. But at least you’ll know which one you’re in.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;&lt;strong&gt;If you’re an engineering leader&lt;/strong&gt;, this one’s on you more than anyone else. You set the incentives, whether you realize it or not. And the problem is that most promotion criteria are basically designed to reward complexity, even when they don’t intend to. “Impact” gets measured by the size and scope of what someone built, which more often than not matters! But what they avoided should also matter.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;So start by changing the questions you ask. In design reviews, instead of “have we thought about scale?”, try &lt;em&gt;“what’s the simplest version we could ship, and what specific signals would tell us we need something more complex?”&lt;/em&gt; That one question changes the game: it makes simplicity the default and puts the burden of proof on complexity, not the other way around!&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;In promotion discussions, push back when someone’s packet is basically a list of impressive-sounding systems. Ask: &lt;em&gt;“Was all of that necessary? Did we actually need a pub/sub system here, or did it just look good on paper?”&lt;/em&gt; And when an engineer on your team ships something clean and simple, help them write the narrative. “Evaluated multiple approaches and chose the simplest one that solved the problem” &lt;em&gt;is&lt;/em&gt; a compelling promotion case, but only if you actually treat it like one.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;One more thing: pay attention to what you celebrate publicly. If every shout-out in your team channel is for the big, complex project, that’s what people will optimize for. Start recognizing the engineer who deleted code. The one who said “we don’t need this yet” and was right.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;At the end of the day, if we keep rewarding complexity and ignoring simplicity, we shouldn’t be surprised when that’s exactly what we get. But the fix isn’t complicated. Which, I guess, is kind of the point.&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://i0.wp.com/terriblesoftware.org/wp-content/uploads/2026/03/chjpdmf0zs9sci9pbwfnzxmvd2vic2l0zs8ymdiylta1l25zmtexndetaw1hz2uta3d2d3b1c3kuanbn.webp?fit=1024%2C680&amp;ssl=1&amp;w=640"/>
        <link href="https://i0.wp.com/terriblesoftware.org/wp-content/uploads/2026/03/chjpdmf0zs9sci9pbwfnzxmvd2vic2l0zs8ymdiylta1l25zmtexndetaw1hz2uta3d2d3b1c3kuanbn.webp?fit=1024%2C680&amp;ssl=1" rel="enclosure" type="image"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://terriblesoftware.org/feed/</id>
            <title type="html">Terrible Software</title>
            <link href="https://terriblesoftware.org" rel="alternate" type="text/html"/>
            <updated>2026-03-07T05:55:34Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbf7aa19d:105d1:68bfe759</id>
        <title type="html">The Frontend Treadmill</title>
        <published>2026-03-05T19:30:10Z</published>
        <updated>2026-03-05T19:30:14Z</updated>
        <link href="https://polotek.net/posts/the-frontend-treadmill/" rel="alternate" type="text/html"/>
        <summary type="html">A lot of frontend teams are very convinced that rewriting their frontend will lead to the promised land. And I am the bearer of bad tidings.
If you are building a product that you hope has longevity, your frontend framework is the least interesting technical decision for you to make. And all of the time you spend arguing about it is wasted energy.
I will die on this hill.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;A lot of frontend teams are very convinced that rewriting their frontend will lead to the promised land. And I am the bearer of bad tidings.&lt;/p&gt;&lt;p&gt;If you are building a product that you hope has longevity, your frontend framework is the least interesting technical decision for you to make. And all of the time you spend arguing about it is wasted energy.&lt;/p&gt;&lt;p&gt;I will die on this hill.&lt;/p&gt;&lt;p&gt;If your product is still around in 5 years, you’re doing great and you should feel successful. But guess what? Whatever framework you choose will be obsolete in 5 years. That’s just how the frontend community has been operating, and I don’t expect it to change soon. Even the popular frameworks that are still around are completely different. Because change is the name of the game. So they’re gonna rewrite their shit too and just give it a new version number.&lt;/p&gt;&lt;p&gt;Product teams that are smart are getting off the treadmill. Whatever framework you currently have, start investing in getting to know it deeply. Learn the tools until they are not an impediment to your progress. That’s the only option. Replacing it with a shiny new tool is a trap.&lt;/p&gt;&lt;p&gt;I also wanna give a piece of candid advice to engineers who are searching for jobs. If you feel strongly about what framework you want to use, please make that a criteria for your job search. Please stop walking into teams and derailing everything by trying to convince them to switch from framework X to your framework of choice. It’s really annoying and tremendously costly.&lt;/p&gt;&lt;p&gt;I always have to start with the cynical take. It’s just how I am. But I do want to talk about what I think should be happening instead.&lt;/p&gt;&lt;p&gt;Companies that want to reduce the cost of their frontend tech becoming obsoleted so often should be looking to get back to fundamentals. Your teams should be working closer to the web platform with a lot less complex abstractions. We need to relearn what the web is capable of and go back to that.&lt;/p&gt;&lt;p&gt;Let’s be clear, I’m not suggesting this is strictly better and the answer to all of your problems. I’m suggesting this as an intentional business tradeoff that I think provides more value and is less costly in the long run. I believe if you stick closer to core web technologies, you’ll be better able to hire capable engineers in the future without them convincing you they can’t do work without rewriting millions of lines of code.&lt;/p&gt;&lt;p&gt;And if you’re an engineer, you will be able to retain much higher market value over time if you dig into and understand core web technologies. I was here before react, and I’ll be here after it dies. You may trade some job marketability today. But it does a lot more for career longevity than trying to learn every new thing that gets popular. And you see how quickly they discarded us when the market turned anyway. Knowing certain tech won’t save you from those realities.&lt;/p&gt;&lt;p&gt;I couldn’t speak this candidly about this stuff when I held a management role. People can’t help but question my motivations and whatever agenda I may be pushing. Either that or I get into a lot of trouble with my internal team because they think I’m talking about them. But this is just what I’ve seen play out after doing this for 20+ years. And I feel like we need to be able to speak plainly.&lt;/p&gt;&lt;p&gt;This has been brewing in my head for a long time. The frontend ecosystem is kind of broken right now. And it’s frustrating to me for a few different reasons. New developers are having an extremely hard time learning enough skills to be gainfully employed. They are drowning in this complex garbage and feeling really disheartened. As a result, companies are finding it more difficult to do basic hiring. The bar is so high just to get a regular dev job. And everybody loses.&lt;/p&gt;&lt;p&gt;What’s even worse is that I believe a lot of this energy is wasted. People that are learning the current tech ecosystem are absolutely not learning web fundamentals. They are too abstracted away. And when the stack changes again, these folks are going to be at a serious disadvantage when they have to adapt away from what they learned. It’s a deep disservice to people’s professional careers, and it’s going to cause a lot of heartache later.&lt;/p&gt;&lt;p&gt;On a more personal note, this is frustrating to me because I think it’s a big part of why we’re seeing the web stagnate so much. I still run into lots of devs who are creative and enthusiastic about building cool things. They just can’t. They are trying and failing because the tools being recommended to them are just not approachable enough. And at the same time, they’re being convinced that learning fundamentals is a waste of time because it’s so different from what everybody is talking about.&lt;/p&gt;&lt;p&gt;I guess I want to close by stating my biases. I’m a web guy. I’ve been bullish on the web for 20+ years, and I will continue to be. I think it is an extremely capable and unique platform for delivering software. And it has only gotten better over time while retaining an incredible level of backwards compatibility. The underlying tools we have are dope now. But our current framework layer is working against the grain instead of embracing the platform.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;This is from &lt;a href="https://social.polotek.net/@polotek/112617458589147547"&gt;a recent thread I wrote on mastodon&lt;/a&gt;. Reproduced with only light editing.&lt;/p&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Marco Rogers (polotek)</name>
        </author>
        <media:content medium="image" url="https://polotek.net/js/script.min.74bf1a3fcf1af396efa4acf3e660e876b61a2153ab9cbe1893ac24ea6d4f94ee.js"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbf6d2731:80d01:4b6775c</id>
        <title type="html">A GitHub Issue Title Compromised 4,000 Developer Machines</title>
        <published>2026-03-05T19:15:27Z</published>
        <updated>2026-03-05T19:15:36Z</updated>
        <link href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another" rel="alternate" type="text/html"/>
        <summary type="html">A prompt injection in a GitHub issue triggered a chain reaction that ended with 4,000 developers getting OpenClaw installed without consent. The attack composes well-understood vulnerabilities into something new: one AI tool bootstrapping another.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;img src="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another/hero-clinejection-chain-1600x900.png" alt="The Clinejection attack chain: a prompt injection in a GitHub issue title cascades through AI triage, cache poisoning, and credential theft to silently install OpenClaw on 4,000 developer machines" tabindex="0"&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;img src="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another/hero-clinejection-chain-1600x900.png" alt="The Clinejection attack chain: a prompt injection in a GitHub issue title cascades through AI triage, cache poisoning, and credential theft to silently install OpenClaw on 4,000 developer machines"&gt;&lt;span&gt;esc to close&lt;/span&gt;&lt;/div&gt;&lt;figcaption&gt;Five steps from a GitHub issue title to 4,000 compromised developer machines. The entry point was natural language.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;On February 17, 2026, someone published &lt;code&gt;cline@2.3.0&lt;/code&gt; to npm. The CLI binary was byte-identical to the previous version. The only change was one line in &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;"postinstall": "npm install -g openclaw@latest"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the next eight hours, every developer who installed or updated Cline got OpenClaw - a separate AI agent with full system access - installed globally on their machine without consent. Approximately 4,000 downloads occurred before the package was pulled&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The interesting part is not the payload. It is how the attacker got the npm token in the first place: by injecting a prompt into a GitHub issue title, which an AI triage bot read, interpreted as an instruction, and executed.&lt;/p&gt;
&lt;h2&gt;The full chain&lt;/h2&gt;
&lt;p&gt;The attack - which Snyk named "Clinejection"&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-2" id="user-content-fnref-2"&gt;2&lt;/a&gt;&lt;/sup&gt; - composes five well-understood vulnerabilities into a single exploit that requires nothing more than opening a GitHub issue.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Prompt injection via issue title.&lt;/strong&gt; Cline had deployed an AI-powered issue triage workflow using Anthropic's &lt;code&gt;claude-code-action&lt;/code&gt;. The workflow was configured with &lt;code&gt;allowed_non_write_users: "*"&lt;/code&gt;, meaning any GitHub user could trigger it by opening an issue. The issue title was interpolated directly into Claude's prompt via &lt;code&gt;${{ github.event.issue.title }}&lt;/code&gt; without sanitisation.&lt;/p&gt;
&lt;p&gt;On January 28, an attacker created Issue #8904 with a title crafted to look like a performance report but containing an embedded instruction: install a package from a specific GitHub repository&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: The AI bot executes arbitrary code.&lt;/strong&gt; Claude interpreted the injected instruction as legitimate and ran &lt;code&gt;npm install&lt;/code&gt; pointing to the attacker's fork - a typosquatted repository (&lt;code&gt;glthub-actions/cline&lt;/code&gt;, note the missing 'i' in 'github'). The fork's &lt;code&gt;package.json&lt;/code&gt; contained a preinstall script that fetched and executed a remote shell script.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Cache poisoning.&lt;/strong&gt; The shell script deployed Cacheract, a GitHub Actions cache poisoning tool. It flooded the cache with over 10GB of junk data, triggering GitHub's LRU eviction policy and evicting legitimate cache entries. The poisoned entries were crafted to match the cache key pattern used by Cline's nightly release workflow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Credential theft.&lt;/strong&gt; When the nightly release workflow ran and restored &lt;code&gt;node_modules&lt;/code&gt; from cache, it got the compromised version. The release workflow held the &lt;code&gt;NPM_RELEASE_TOKEN&lt;/code&gt;, &lt;code&gt;VSCE_PAT&lt;/code&gt; (VS Code Marketplace), and &lt;code&gt;OVSX_PAT&lt;/code&gt; (OpenVSX). All three were exfiltrated&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3-2"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Malicious publish.&lt;/strong&gt; Using the stolen npm token, the attacker published &lt;code&gt;cline@2.3.0&lt;/code&gt; with the OpenClaw postinstall hook. The compromised version was live for eight hours before StepSecurity's automated monitoring flagged it - approximately 14 minutes after publication&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1-2"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2&gt;A botched rotation made it worse&lt;/h2&gt;
&lt;p&gt;Security researcher Adnan Khan had actually discovered the vulnerability chain in late December 2025 and reported it via a GitHub Security Advisory on January 1, 2026. He sent multiple follow-ups over five weeks. None received a response&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3-3"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;When Khan publicly disclosed on February 9, Cline patched within 30 minutes by removing the AI triage workflows. They began credential rotation the next day.&lt;/p&gt;
&lt;p&gt;But the rotation was incomplete. The team deleted the wrong token, leaving the exposed one active&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-4" id="user-content-fnref-4"&gt;4&lt;/a&gt;&lt;/sup&gt;. They discovered the error on February 11 and re-rotated. But the attacker had already exfiltrated the credentials, and the npm token remained valid long enough to publish the compromised package six days later.&lt;/p&gt;
&lt;p&gt;Khan was not the attacker. A separate, unknown actor found Khan's proof-of-concept on his test repository and weaponised it against Cline directly&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3-4"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2&gt;The new pattern: AI installs AI&lt;/h2&gt;
&lt;p&gt;The specific vulnerability chain is interesting but not unprecedented. Prompt injection, cache poisoning, and credential theft are all documented attack classes. What makes Clinejection distinct is the outcome: one AI tool silently bootstrapping a second AI agent on developer machines.&lt;/p&gt;
&lt;p&gt;This creates a recursion problem in the supply chain. The developer trusts Tool A (Cline). Tool A is compromised to install Tool B (OpenClaw). Tool B has its own capabilities - shell execution, credential access, persistent daemon installation - that are independent of Tool A and invisible to the developer's original trust decision.&lt;/p&gt;
&lt;p&gt;OpenClaw as installed could read credentials from &lt;code&gt;~/.openclaw/&lt;/code&gt;, execute shell commands via its Gateway API, and install itself as a persistent system daemon surviving reboots&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1-3"&gt;1&lt;/a&gt;&lt;/sup&gt;. The severity was debated - Endor Labs characterised the payload as closer to a proof-of-concept than a weaponised attack&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-5" id="user-content-fnref-5"&gt;5&lt;/a&gt;&lt;/sup&gt; - but the mechanism is what matters. The next payload will not be a proof-of-concept.&lt;/p&gt;
&lt;p&gt;This is the supply chain equivalent of &lt;a href="https://en.wikipedia.org/wiki/Confused_deputy_problem"&gt;confused deputy&lt;/a&gt;: the developer authorises Cline to act on their behalf, and Cline (via compromise) delegates that authority to an entirely separate agent the developer never evaluated, never configured, and never consented to.&lt;/p&gt;
&lt;h2&gt;Why existing controls did not catch it&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;npm audit&lt;/strong&gt;: The postinstall script installs a legitimate, non-malicious package (OpenClaw). There is no malware to detect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code review&lt;/strong&gt;: The CLI binary was byte-identical to the previous version. Only &lt;code&gt;package.json&lt;/code&gt; changed, and only by one line. Automated diff checks that focus on binary changes would miss it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Provenance attestations&lt;/strong&gt;: Cline was not using OIDC-based npm provenance at the time. The compromised token could publish without provenance metadata, which StepSecurity flagged as anomalous&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1-4"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Permission prompts&lt;/strong&gt;: The installation happens in a postinstall hook during &lt;code&gt;npm install&lt;/code&gt;. No AI coding tool prompts the user before a dependency's lifecycle script runs. The operation is invisible.&lt;/p&gt;
&lt;p&gt;The attack exploited the gap between what developers think they are installing (a specific version of Cline) and what actually executes (arbitrary lifecycle scripts from the package and everything it transitively installs).&lt;/p&gt;
&lt;h2&gt;What Cline changed afterward&lt;/h2&gt;
&lt;p&gt;Cline's post-mortem&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-4" id="user-content-fnref-4-2"&gt;4&lt;/a&gt;&lt;/sup&gt; outlines several remediation steps:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Eliminated GitHub Actions cache usage from credential-handling workflows&lt;/li&gt;
&lt;li&gt;Adopted OIDC provenance attestations for npm publishing, eliminating long-lived tokens&lt;/li&gt;
&lt;li&gt;Added verification requirements for credential rotation&lt;/li&gt;
&lt;li&gt;Began working on a formal vulnerability disclosure process with SLAs&lt;/li&gt;
&lt;li&gt;Commissioned third-party security audits of CI/CD infrastructure&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;These are meaningful improvements. The OIDC migration alone would have prevented the attack - a stolen token cannot publish packages when provenance requires a cryptographic attestation from a specific GitHub Actions workflow.&lt;/p&gt;
&lt;h2&gt;The architectural question&lt;/h2&gt;
&lt;p&gt;Clinejection is a supply chain attack, but it is also an agent security problem. The entry point was natural language in a GitHub issue title. The first link in the chain was an AI bot that interpreted untrusted text as an instruction and executed it with the privileges of the CI environment.&lt;/p&gt;
&lt;p&gt;This is the same structural pattern we have written about in the context of &lt;a href="https://grith.ai/blog/mcp-servers-new-npm-packages"&gt;MCP tool poisoning&lt;/a&gt; and &lt;a href="https://grith.ai/blog/agent-skills-supply-chain"&gt;agent skill registries&lt;/a&gt; - untrusted input reaches an agent, the agent acts on it, and nothing evaluates the resulting operations before they execute.&lt;/p&gt;
&lt;p&gt;The difference here is that the agent was not a developer's local coding assistant. It was an automated CI workflow that ran on every new issue, with shell access and cached credentials. The blast radius was not one developer's machine - it was the entire project's publication pipeline.&lt;/p&gt;
&lt;p&gt;Every team deploying AI agents in CI/CD - for issue triage, code review, automated testing, or any other workflow - has this same exposure. The agent processes untrusted input (issues, PRs, comments) and has access to secrets (tokens, keys, credentials). The question is whether anything evaluates what the agent does with that access.&lt;/p&gt;
&lt;p&gt;Per-syscall interception catches this class of attack at the operation layer. When the AI triage bot attempts to run &lt;code&gt;npm install&lt;/code&gt; from an unexpected repository, the operation is evaluated against policy before it executes - regardless of what the issue title said. When a lifecycle script attempts to exfiltrate credentials to an external host, the egress is blocked.&lt;/p&gt;
&lt;p&gt;The entry point changes. The operations do not. &lt;a href="https://grith.ai/"&gt;grith&lt;/a&gt; was built to catch exactly this class of problem - evaluating every operation at the syscall layer, regardless of which agent triggered it or why.&lt;/p&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another/hero-clinejection-chain-1600x900.png"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbc9c1a2c:32fe9b:98d097d6</id>
        <title type="html">Container queries and units in action</title>
        <published>2026-03-05T06:07:52Z</published>
        <updated>2026-03-05T06:07:56Z</updated>
        <link href="https://web.dev/articles/baseline-in-action-container-queries" rel="alternate" type="text/html"/>
        <summary type="html">Learn how to use CSS container queries for adding both responsive typography and styles based on container dimensions in this guide.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;

  
    
    




&lt;p&gt;

&lt;/p&gt;

&lt;p&gt;
  Published: October 23, 2025
&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;
   
&lt;/p&gt;

&lt;p&gt;One of the goals when writing CSS is to build component parts that will adapt well to different (and unexpected) contexts. Ideally, a component can be placed inside any "container" element without it feeling broken or out of place. How can you accomplish this in a complex layout like a store where the primary component—the "product"—has to fit into a variety of list layouts, including the sidebar?&lt;/p&gt;

&lt;h2 id="responsive_typography_with_cqi_units" tabindex="-1"&gt;&lt;span&gt;Responsive typography with &lt;code dir="ltr"&gt;cqi&lt;/code&gt; units&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;The first step is to define some basic sizing variables that could be reused across the project—starting with whitespace sizes. But before creating any custom properties, the browser provides some useful named values as CSS units:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;&lt;code dir="ltr"&gt;1em&lt;/code&gt;: the current font size.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1rem&lt;/code&gt;: the font size on the &lt;code dir="ltr"&gt;:root (html)&lt;/code&gt; element.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1lh&lt;/code&gt; / &lt;code dir="ltr"&gt;1rlh&lt;/code&gt;: the current and root line heights.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1vw&lt;/code&gt;: the viewport width.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1vi&lt;/code&gt;: the viewport "inline" size (for English, this is the same as &lt;code dir="ltr"&gt;vw&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1cqi&lt;/code&gt;: the inline size of the nearest "container" (defaulting to the viewport).&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;You can think of these units as common variables provided by the browser, with a shorthand syntax for multiplication. If you wanted half of a &lt;code dir="ltr"&gt;--line-height&lt;/code&gt; custom property in CSS, you would need to write the entire calculation &lt;code dir="ltr"&gt;calc(0.5 * var(--line-height))&lt;/code&gt;, but with the &lt;code dir="ltr"&gt;lh&lt;/code&gt; unit, you can ask for &lt;code dir="ltr"&gt;0.5lh&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;Custom properties like &lt;code dir="ltr"&gt;--brand-color&lt;/code&gt; and &lt;code dir="ltr"&gt;--button-background&lt;/code&gt; have different meanings and serve a different purpose, even when they result in the same &lt;code dir="ltr"&gt;deepPink&lt;/code&gt; color (another browser-provided variable). Similarly, &lt;code dir="ltr"&gt;1em&lt;/code&gt; might sometimes be equal to &lt;code dir="ltr"&gt;16px&lt;/code&gt;, but that's not a stable relationship. Like any other variable, units should be used to express a relationship, rather than an expected value.&lt;/p&gt;

&lt;p&gt;Both &lt;code dir="ltr"&gt;1em&lt;/code&gt; and &lt;code dir="ltr"&gt;1lh&lt;/code&gt; are font-relative units that could be used for spacing, but only one of them has a reliable relationship to the current line height. If this page involved a lot of elements with prose in them—paragraphs and lists, for example—the &lt;code dir="ltr"&gt;lh&lt;/code&gt; unit would work well for spacing between them:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ul&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ol&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;margin-block&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;lh&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That will maintain a consistent baseline rhythm, without any extra work. But this page has almost no prose. Instead of spacing within a flow of text, this layout requires spacing to push text away from the edge of a card, and spacing between columns and rows in a grid or stacked layout. In this case there are several things to consider:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;Multiples (or fractions) of &lt;code dir="ltr"&gt;1lh&lt;/code&gt; may still be useful for maintaining vertical rhythm across the page.&lt;/li&gt;
&lt;li&gt;The &lt;code dir="ltr"&gt;cqi&lt;/code&gt; unit would account for the amount of &lt;em&gt;available space&lt;/em&gt; in a given context.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;By combining these two units in a &lt;code dir="ltr"&gt;round()&lt;/code&gt; function, the &lt;code dir="ltr"&gt;--gap&lt;/code&gt; variable is based primarily on the container size, but rounded up to a multiple of quarter-lines:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--gap&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;round&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;up&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;cqi&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.25&lt;/span&gt;&lt;span&gt;lh&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;If the &lt;code dir="ltr"&gt;line-height&lt;/code&gt; is &lt;code dir="ltr"&gt;20px&lt;/code&gt;, then the &lt;code dir="ltr"&gt;--&lt;/code&gt;gap will be multiples of &lt;code dir="ltr"&gt;5px&lt;/code&gt;—but the exact multiple will depend on available space. If either the &lt;code dir="ltr"&gt;line-height&lt;/code&gt; or available space change, the &lt;code dir="ltr"&gt;--gap&lt;/code&gt; variable will adapt to its new context. To make the &lt;code dir="ltr"&gt;--gap&lt;/code&gt; consistent across the entire design, you could replace the container-relative &lt;code dir="ltr"&gt;cqi&lt;/code&gt; units with viewport-relative &lt;code dir="ltr"&gt;vi&lt;/code&gt; units.&lt;/p&gt;

&lt;p&gt;The same approach is useful when establishing "fluid" font sizes that respond to available space. This &lt;code dir="ltr"&gt;--body-text&lt;/code&gt; variable is based on the user-provided font preference, with some range to adapt based on the container. In this case, &lt;code dir="ltr"&gt;clamp()&lt;/code&gt; ensures the font will be at least as large as the user's preference, can grow some as the container size increases, but will stop growing at some point:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--body-text&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clamp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;rem&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.875&lt;/span&gt;&lt;span&gt;rem&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;cqi&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1.25&lt;/span&gt;&lt;span&gt;rem&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;&lt;li&gt;The range is clamped between &lt;code dir="ltr"&gt;1rem&lt;/code&gt; and &lt;code dir="ltr"&gt;1.25rem&lt;/code&gt;, to stay near the user-selected font size.&lt;/li&gt;
&lt;li&gt;The &lt;code dir="ltr"&gt;cqi&lt;/code&gt; value determines how &lt;em&gt;fast&lt;/em&gt; the font will grow or shrink in relation to available space, which is added to a &lt;code dir="ltr"&gt;rem&lt;/code&gt; value to offset that growth based on the user-selected size.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Keeping the central &lt;code dir="ltr"&gt;rem&lt;/code&gt; value near &lt;code dir="ltr"&gt;1&lt;/code&gt; and the added &lt;code dir="ltr"&gt;cqi&lt;/code&gt; value low ensures that users still have significant control when zooming in or out. You can adjust those two values, and then use browser zoom to see how they interact. The closer you get to &lt;code dir="ltr"&gt;100cqi&lt;/code&gt;, and the farther you fall below &lt;code dir="ltr"&gt;1rem&lt;/code&gt;, the less influence user font preferences will have—and the less font sizes will respond to zooming in or out.&lt;/p&gt;

&lt;p&gt;The &lt;code dir="ltr"&gt;--item-title&lt;/code&gt; variable is slightly larger and more responsive, and the &lt;code dir="ltr"&gt;--list-title&lt;/code&gt; size responds to the viewport size (using &lt;code dir="ltr"&gt;vi&lt;/code&gt;) rather than the immediate container size. That way item headings respond to their context, but the main list headings all match in size no matter where they show up.&lt;/p&gt;
&lt;aside&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;span&gt; Product titles in the cart sidebar might be slightly different from product titles in the main list—but the "cart" heading is always identical to the "products" list heading.&lt;/span&gt;&lt;/aside&gt;&lt;h2 id="defining_containers_to_measure_in_context" tabindex="-1"&gt;&lt;span&gt;Defining containers to measure in context&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;At this point, container query units have been used to create adaptive typography on a web page, but new containers haven't been defined.&lt;/p&gt;

&lt;p&gt;By default, &lt;code dir="ltr"&gt;1cqi&lt;/code&gt; (&lt;code dir="ltr"&gt;1/100&lt;/code&gt; container query inline size) is the same as &lt;code dir="ltr"&gt;1svi&lt;/code&gt; (&lt;code dir="ltr"&gt;1/100&lt;/code&gt; small viewport inline size) because the &lt;a href="https://web.dev/learn/css/sizing#alternative_viewport-relative_units"&gt;"small" viewport&lt;/a&gt; acts as the initial container for any web page. In order to take full advantage of the &lt;code dir="ltr"&gt;cqi&lt;/code&gt; unit, you need to define additional "containers" within the page. The primary layout containers on this page are the &lt;code dir="ltr"&gt;product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt;—so they are set to expose their &lt;code dir="ltr"&gt;inline-size&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;product-list&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;shopping-cart&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;container-type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;inline&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;aside&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;span&gt; &lt;code dir="ltr"&gt;&amp;lt;div&amp;gt;&lt;/code&gt; or &lt;code dir="ltr"&gt;&amp;lt;section&amp;gt;&lt;/code&gt; elements with &lt;code dir="ltr"&gt;.product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;.shopping-cart&lt;/code&gt; classes would also work here. Since those have no semantic meaning or functionality in HTML, it also works to define unregistered custom elements with those names. This doesn't impact the CSS, except that custom elements use a &lt;code dir="ltr"&gt;display&lt;/code&gt; of &lt;code dir="ltr"&gt;inline&lt;/code&gt; by default.&lt;/span&gt;&lt;/aside&gt;&lt;p&gt;Container query units—including &lt;code dir="ltr"&gt;cqi&lt;/code&gt;—aren't able to measure the element that they are used on. If you set the &lt;code dir="ltr"&gt;width&lt;/code&gt; of &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; to &lt;code dir="ltr"&gt;25cqi&lt;/code&gt;, it would be a paradox to determine the container's width based on its own width! Instead, the result will be based on the next ancestor container in the tree hierarchy &lt;em&gt;that contains&lt;/em&gt; &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; cards are part of a &lt;code dir="ltr"&gt;product-list&lt;/code&gt; grid that can be changed by the user. The &lt;code dir="ltr"&gt;list&lt;/code&gt; layout option displays each card at full width. In that case, referring to the parent container size is useful, but both the &lt;code dir="ltr"&gt;small-grid&lt;/code&gt; and &lt;code dir="ltr"&gt;large-grid&lt;/code&gt; options cause the card to grow and shrink depending on how many columns fit into the container. Even when the container is quite large, the cards can remain tightly packed into smaller grid cells.&lt;/p&gt;

&lt;p&gt;There's currently no way to declare those grid cells as "containers" directly. Instead, an extra element is needed to measure within each cell. That's why each &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; instance has an &lt;code dir="ltr"&gt;&amp;lt;article&amp;gt;&lt;/code&gt; element nested directly inside. &lt;code dir="ltr"&gt;product-detail &amp;gt; article&lt;/code&gt; is the card to be styled, while &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; itself is used only as a container to measure. That allows the &lt;code dir="ltr"&gt;cqi&lt;/code&gt;-based text and spacing calculations previously defined to be recalculated for the space available to each card.&lt;/p&gt;

&lt;h2 id="explicit_container_queries" tabindex="-1"&gt;&lt;span&gt;Explicit container queries&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;Container units are powerful, but sometimes it's useful to make more dramatic changes in a component layout when the available size crosses a threshold. These are often called &lt;em&gt;breakpoints&lt;/em&gt;—since the fix is applied at the point when a given layout begins to break. You may already be familiar with using &lt;code dir="ltr"&gt;@media&lt;/code&gt; to add breakpoints based on the viewport size:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grid&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'controls'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'cart'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'list'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;minmax&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;-content&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;

&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@media&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(width&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;30em)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'controls controls'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'list cart'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; appears above the main &lt;code dir="ltr"&gt;product-list&lt;/code&gt; on small screens—but at a certain point, there's more horizontal space, and a sidebar makes more sense. Since that shift depends on the overall viewport, a media query is used to handle the change. However, the &lt;code dir="ltr"&gt;product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; components might appear in different contexts, somewhat independent of the viewport size. When the viewport grows wider than the sidebar breakpoint, the &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; component suddenly becomes &lt;em&gt;smaller&lt;/em&gt;, providing less space for products inside. This is where container queries become necessary. The syntax is nearly identical to a media query, but uses &lt;code dir="ltr"&gt;@container&lt;/code&gt; rather than &lt;code dir="ltr"&gt;@media&lt;/code&gt; at the start of the rule:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;article&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grid&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'image'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;'title'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'summary'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'button'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@container&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(inline-size&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;40ch)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;--image-ratio&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image title'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image summary'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image button'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;minmax&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&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;/span&gt;&lt;span&gt;container&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;inline-size&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;50ch&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;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image title title'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image summary button'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1fr&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;minmax&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50px&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;%,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;500px&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1fr&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fit-content&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;20&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;The initial goal of this post is that components should be able to respond to any context. To make that work, each component defines its own internal behavior, without explicit knowledge of the surrounding components. Container queries help us accomplish that. The &lt;code dir="ltr"&gt;product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; components don't need to be aware of why they have more or less space in a given context, they only need to know &lt;em&gt;how much space&lt;/em&gt; is currently available.&lt;/p&gt;

&lt;h2 id="transition_grid_templates_and_visibility" tabindex="-1"&gt;&lt;span&gt;Transition grid templates and visibility&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;With a layout that's changing often based on container queries, how do we smooth out all of the transitions? When using grids for layout, it's possible to animate the size of a column or row, as well as the gap between columns and rows. In this case, the cart area and gap are expanded from &lt;code dir="ltr"&gt;0&lt;/code&gt; width when the cart is opened. In order to animate grid templates like this, two things are required:&lt;/p&gt;

&lt;ol&gt;&lt;li&gt;The initial and end states must have the same number of tracks (columns or rows).&lt;/li&gt;
&lt;li&gt;The animated tracks must use comparable units.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;While it would be possible to change from a one-column grid to a two-column grid when the sidebar is hidden, the empty sidebar column is instead resized to &lt;code dir="ltr"&gt;0&lt;/code&gt;, and the column-gap is also set to &lt;code dir="ltr"&gt;0&lt;/code&gt;. When the cart is open in the sidebar, the &lt;code dir="ltr"&gt;0&lt;/code&gt;-width column transitions to &lt;code dir="ltr"&gt;calc(15em + 1cqi)&lt;/code&gt;. Since the calculation results in a normal length value, the transition can be animated from length &lt;code dir="ltr"&gt;0&lt;/code&gt;. The gap also animates from one length to another—from &lt;code dir="ltr"&gt;0&lt;/code&gt; to &lt;code dir="ltr"&gt;var(--gap)&lt;/code&gt;—which was defined earlier:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;transition&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;250&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gap&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;250&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="non-baseline_animation_enhancements" tabindex="-1"&gt;&lt;span&gt;Non-Baseline animation enhancements&lt;/span&gt;&lt;/h3&gt;

&lt;p&gt;The &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; and &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; components are also animated when hidden or shown, and this is done using two recent features that are not yet Baseline, but work as progressive enhancements for browsers that do have support:&lt;/p&gt;

&lt;ol&gt;&lt;li&gt;Applying &lt;code dir="ltr"&gt;interpolate-size: allow-keywords&lt;/code&gt; allows &lt;a href="https://developer.chrome.com/docs/css-ui/animate-to-height-auto#animate_to_and_from_intrinsic_sizing_keywords_with_interpolate-size"&gt;transitioning element dimensions from &lt;code dir="ltr"&gt;0&lt;/code&gt; to &lt;code dir="ltr"&gt;auto&lt;/code&gt;&lt;/a&gt;. This is used for transitioning the products to and from a &lt;code dir="ltr"&gt;block-size&lt;/code&gt; of &lt;code dir="ltr"&gt;0&lt;/code&gt; when they are added or removed from the cart. Since the &lt;code dir="ltr"&gt;interpolate-size&lt;/code&gt; property inherits, that only needs to be defined once on the &lt;code dir="ltr"&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element, and it is available to every other element on the page. This is only supported in Chrome-based browsers (Chrome, Edge, and others), but can be used as a progressive enhancement. The fallback works as expected, just without the animated transition.&lt;br&gt;&lt;/li&gt;
&lt;li&gt;"Discrete" properties like &lt;code dir="ltr"&gt;display&lt;/code&gt; can now be transitioned as well, even though there are no intermediate values between &lt;code dir="ltr"&gt;grid&lt;/code&gt; and &lt;code dir="ltr"&gt;none&lt;/code&gt;. Instead the transition is applied at the start or end of the duration provided. To achieve that, &lt;code dir="ltr"&gt;allow-discrete&lt;/code&gt; is added to the &lt;code dir="ltr"&gt;transition-behavior&lt;/code&gt; property. While &lt;code dir="ltr"&gt;transition-behavior&lt;/code&gt; is otherwise Baseline, animating the &lt;code dir="ltr"&gt;display&lt;/code&gt; property is not yet supported in Firefox. But again, this works well as a progressive enhancement.&lt;br&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;There are many situations where container queries and units can be used to replace media queries and viewport units, and that's great when it helps you express the intent of a design more clearly. But one is not meant to replace the other, and there's not a &lt;em&gt;better&lt;/em&gt; query or unit that will work in every situation. CSS works best when the relationships established in code match the goals and purpose of the design. When you want text and spacing that is relative to immediate context, container queries provide that functionality, but when you want consistent sizing relative to the overall viewport, media queries and viewport units are still an excellent option. Most websites will likely involve a mix of both.&lt;/p&gt;
  

  
&lt;/div&gt;

  
    
    
      
    &lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://web.dev/static/articles/baseline-in-action-container-queries/image/thumbnail.png"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbc9930f7:30dbfd:20d3c2a3</id>
        <title type="html">CSS @scope: An Alternative To Naming Conventions And Heavy Abstractions</title>
        <published>2026-03-05T06:04:42Z</published>
        <updated>2026-03-05T06:04:47Z</updated>
        <link href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/" rel="alternate" type="text/html"/>
        <summary type="html">Prescriptive class name conventions are no longer enough to keep CSS maintainable in a world of increasingly complex interfaces. Can the new `@scope` rule finally give developers the confidence to write CSS that can keep up with modern front ends?</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;header&gt;&lt;h1 id="main-heading"&gt;CSS &lt;code&gt;@scope&lt;/code&gt;: An Alternative To Naming Conventions And Heavy Abstractions&lt;/h1&gt;&lt;/header&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;section&gt;Prescriptive class name conventions are no longer enough to keep CSS maintainable in a world of increasingly complex interfaces. Can the new &lt;code&gt;@scope&lt;/code&gt; rule finally give developers the confidence to write CSS that can keep up with modern front ends?&lt;/section&gt;&lt;/p&gt;&lt;p&gt;When learning the principles of basic CSS, one is taught to write modular, reusable, and descriptive styles to ensure maintainability. But when developers become involved with real-world applications, it often feels impossible to add UI features without styles leaking into unintended areas.&lt;/p&gt;&lt;p&gt;This issue often snowballs into a self-fulfilling loop; styles that are theoretically scoped to one element or class start showing up where they don’t belong. This forces the developer to create even more specific selectors to override the leaked styles, which then accidentally override global styles, and so on.&lt;/p&gt;&lt;p&gt;Rigid class name conventions, such as &lt;a href="https://getbem.com/introduction/"&gt;BEM&lt;/a&gt;, are one theoretical solution to this issue. The &lt;strong&gt;BEM (Block, Element, Modifier) methodology&lt;/strong&gt; is a &lt;a href="https://www.smashingmagazine.com/2012/04/a-new-front-end-methodology-bem/"&gt;systematic way of naming CSS classes&lt;/a&gt; to ensure reusability and structure within CSS files. Naming conventions like this can &lt;a href="https://www.smashingmagazine.com/2018/06/bem-for-beginners/"&gt;reduce cognitive load by leveraging domain language to describe elements and their state&lt;/a&gt;, and if implemented correctly, &lt;a href="https://www.smashingmagazine.com/2025/06/css-cascade-layers-bem-utility-classes-specificity-control/"&gt;can make styles for large applications easier to maintain&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;In the real world, however, it doesn’t always work out like that. Priorities can change, and with change, implementation becomes inconsistent. Small changes to the HTML structure can require many CSS class name revisions. With highly interactive front-end applications, class names following the BEM pattern can become long and unwieldy (e.g., &lt;code&gt;app-user-overview__status--is-authenticating&lt;/code&gt;), and not fully adhering to the naming rules breaks the system’s structure, thereby negating its benefits.&lt;/p&gt;&lt;p&gt;Given these challenges, it’s no wonder that developers have turned to frameworks, Tailwind being &lt;a href="https://2024.stateofcss.com/en-US/tools/"&gt;the most popular CSS framework&lt;/a&gt;. Rather than trying to fight what seems like an unwinnable specificity war between styles, it is easier to give up on the &lt;a href="https://css-tricks.com/the-c-in-css-the-cascade/"&gt;CSS Cascade&lt;/a&gt; and use tools that guarantee complete isolation.&lt;/p&gt;&lt;h2 id="developers-lean-more-on-utilities"&gt;Developers Lean More On Utilities &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#developers-lean-more-on-utilities"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;How do we know that some developers are keen on avoiding cascaded styles? It’s the rise of “modern” front-end tooling — like &lt;a href="https://www.smashingmagazine.com/2016/04/finally-css-javascript-meet-cssx/"&gt;CSS-in-JS frameworks&lt;/a&gt; — designed specifically for that purpose. Working with isolated styles that are tightly scoped to specific components can seem like a breath of fresh air. It removes the need to name things — &lt;a href="https://24ways.org/2014/naming-things/"&gt;still one of the most hated and time-consuming front-end tasks&lt;/a&gt; — and allows developers to be productive without fully understanding or leveraging the benefits of CSS inheritance.&lt;/p&gt;&lt;p&gt;But ditching the CSS Cascade comes with its own problems. For instance, composing styles in JavaScript requires heavy build configurations and often leads to styles awkwardly intermingling with component markup or HTML. Instead of carefully considered naming conventions, we allow build tools to autogenerate selectors and identifiers for us (e.g., &lt;code&gt;.jsx-3130221066&lt;/code&gt;), requiring developers to keep up with yet another pseudo-language in and of itself. (As if the cognitive load of understanding what all your component’s &lt;code&gt;useEffect&lt;/code&gt;s do weren’t already enough!)&lt;/p&gt;&lt;p&gt;Further abstracting the job of naming classes to tooling means that basic debugging is often constrained to specific application versions compiled for development, rather than leveraging native browser features that support live debugging, such as Developer Tools.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;a href="https://twitter.com/share?text=%0aIt%e2%80%99s%20almost%20like%20we%20need%20to%20develop%20tools%20to%20debug%20the%20tools%20we%e2%80%99re%20using%20to%20abstract%20what%20the%20web%20already%20provides%20%e2%80%94%20all%20for%20the%20sake%20of%20running%20away%20from%20the%20%e2%80%9cpain%e2%80%9d%20of%20writing%20standard%20CSS.%0a&amp;amp;url=https://smashingmagazine.com%2f2026%2f02%2fcss-scope-alternative-naming-conventions%2f"&gt;It’s almost like we need to develop tools to debug the tools we’re using to abstract what the web already provides — all for the sake of running away from the “pain” of writing standard CSS.&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Luckily, modern CSS features not only make writing standard CSS more flexible but also give developers like us a great deal more power to manage the cascade and make it work for us. &lt;a href="https://www.smashingmagazine.com/2022/01/introduction-css-cascade-layers/"&gt;CSS Cascade Layers&lt;/a&gt; are a great example, but there’s another feature that gets a surprising lack of attention — although that is changing now that it has recently become &lt;strong&gt;Baseline compatible&lt;/strong&gt;.&lt;/p&gt;&lt;h2 id="the-css-scope-at-rule"&gt;The CSS &lt;code&gt;@scope&lt;/code&gt; At-Rule &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#the-css-scope-at-rule"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I consider the &lt;strong&gt;CSS &lt;code&gt;@scope&lt;/code&gt; at-rule&lt;/strong&gt; to be a potential cure for the sort of style-leak-induced anxiety we’ve covered, one that does not force us to compromise native web advantages for abstractions and extra build tooling.&lt;/p&gt;&lt;blockquote&gt;“The &lt;code&gt;@scope&lt;/code&gt; CSS at-rule enables you to select elements in specific DOM subtrees, targeting elements precisely without writing overly-specific selectors that are hard to override, and without coupling your selectors too tightly to the DOM structure.”&lt;br&gt;&lt;br&gt;— &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@scope"&gt;MDN&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;In other words, we can work with isolated styles in specific instances &lt;strong&gt;without sacrificing inheritance, cascading, or even the basic separation of concerns&lt;/strong&gt; that has been a long-running guiding principle of front-end development.&lt;/p&gt;&lt;p&gt;Plus, it has &lt;a href="https://caniuse.com/css-cascade-scope"&gt;excellent browser coverage&lt;/a&gt;. In fact, &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/146"&gt;Firefox 146&lt;/a&gt; added support for &lt;code&gt;@scope&lt;/code&gt; in December, making it &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility"&gt;Baseline compatible&lt;/a&gt; for the first time. Here is a simple comparison between a button using the BEM pattern versus the &lt;code&gt;@scope&lt;/code&gt; rule:&lt;/p&gt;&lt;p&gt;The &lt;code&gt;@scope&lt;/code&gt; rule allows for &lt;strong&gt;precision with less complexity&lt;/strong&gt;. The developer no longer needs to create boundaries using class names, which, in turn, allows them to write selectors based on native HTML elements, thereby eliminating the need for prescriptive CSS class name patterns. By simply removing the need for class name management, &lt;code&gt;@scope&lt;/code&gt; can alleviate the fear associated with CSS in large projects.&lt;/p&gt;&lt;h2 id="basic-usage"&gt;Basic Usage &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#basic-usage"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;To get started, add the &lt;code&gt;@scope&lt;/code&gt; rule to your CSS and insert a root selector to which styles will be scoped:&lt;/p&gt;&lt;p&gt;So, for example, if we were to scope styles to a &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; element, it may look something like this:&lt;/p&gt;&lt;p&gt;This, on its own, is not a groundbreaking feature. However, a second argument can be added to the scope to create a &lt;strong&gt;lower boundary&lt;/strong&gt;, effectively defining the scope’s start and end points.&lt;/p&gt;&lt;p&gt;This practice is called &lt;strong&gt;donut scoping&lt;/strong&gt;, and &lt;a href="https://css-tricks.com/solved-by-css-donuts-scopes/"&gt;there are several approaches&lt;/a&gt; one could use, including a series of similar, highly specific selectors coupled tightly to the DOM structure, a &lt;code&gt;:not&lt;/code&gt; pseudo-selector, or assigning specific class names to &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; elements within the &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; to handle the differing CSS.&lt;/p&gt;&lt;p&gt;Regardless of those other approaches, the &lt;code&gt;@scope&lt;/code&gt; method is much more concise. More importantly, it prevents the risk of broken styles if classnames change or are misused or if the HTML structure were to be modified. Now that &lt;code&gt;@scope&lt;/code&gt; is Baseline compatible, we no longer need workarounds!&lt;/p&gt;&lt;p&gt;We can take this idea further with multiple end boundaries to create a “style figure eight”:&lt;/p&gt;&lt;p&gt;Compare that to a version handled without the &lt;code&gt;@scope&lt;/code&gt; rule, where the developer has to “reset” styles to their defaults:&lt;/p&gt;&lt;p&gt;Check out the following example. Do you notice how simple it is to target some nested selectors while exempting others?&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;/p&gt;&lt;figcaption&gt;See the Pen &lt;a href="https://codepen.io/smashingmag/pen/wBWXggN"&gt;@scope example [forked]&lt;/a&gt; by &lt;a href="https://codepen.io/blakeeric"&gt;Blake Lundquist&lt;/a&gt;.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Consider a scenario where unique styles need to be applied to slotted content within &lt;a href="https://www.smashingmagazine.com/2025/07/web-components-working-with-shadow-dom/"&gt;web components&lt;/a&gt;. When slotting content into a web component, that content becomes part of the Shadow DOM, but still inherits styles from the parent document. The developer might want to implement different styles depending on which web component the content is slotted into:&lt;/p&gt;&lt;p&gt;In this example, the developer might want the &lt;code&gt;&amp;lt;user-card&amp;gt;&lt;/code&gt; to have distinct styles only if it is rendered inside &lt;code&gt;&amp;lt;team-roster&amp;gt;&lt;/code&gt;:&lt;/p&gt;&lt;h2 id="more-benefits"&gt;More Benefits &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#more-benefits"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;There are additional ways that &lt;code&gt;@scope&lt;/code&gt; can remove the need for class management without resorting to utilities or JavaScript-generated class names. For example, &lt;code&gt;@scope&lt;/code&gt; opens up the possibility to easily &lt;strong&gt;target descendants of any selector&lt;/strong&gt;, not just class names:&lt;/p&gt;&lt;p&gt;And they &lt;strong&gt;can be nested&lt;/strong&gt;, creating scopes within scopes:&lt;/p&gt;&lt;p&gt;Plus, the root scope can be easily referenced within the &lt;code&gt;@scope&lt;/code&gt; rule:&lt;/p&gt;&lt;p&gt;The &lt;code&gt;@scope&lt;/code&gt; at-rule also introduces a new &lt;strong&gt;proximity&lt;/strong&gt; dimension to CSS specificity resolution. In traditional CSS, when two selectors match the same element, the selector with the higher specificity wins. With &lt;code&gt;@scope&lt;/code&gt;, when two elements have equal specificity, the one whose scope root is closer to the matched element wins. This eliminates the need to override parent styles by manually increasing an element’s specificity, since inner components naturally supersede outer element styles.&lt;/p&gt;&lt;h2 id="conclusion"&gt;Conclusion &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#conclusion"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Utility-first CSS frameworks, such as Tailwind, work well for prototyping and smaller projects. Their benefits quickly diminish, however, when used in larger projects involving more than a couple of developers.&lt;/p&gt;&lt;p&gt;Front-end development has become increasingly overcomplicated in the last few years, and CSS is no exception. While the &lt;code&gt;@scope&lt;/code&gt; rule isn’t a cure-all, it can reduce the need for complex tooling. When used in place of, or alongside strategic class naming, &lt;code&gt;@scope&lt;/code&gt; can make it easier and more fun to write maintainable CSS.&lt;/p&gt;&lt;h3 id="further-reading"&gt;Further Reading &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#further-reading"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial"&gt;&lt;span&gt;(gg, yk)&lt;/span&gt;&lt;/div&gt;&lt;p&gt;&lt;span&gt;Explore more on&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>About The Author</name>
        </author>
        <media:content medium="image" url="https://files.smashing.media/articles/css-scope-alternative-naming-conventions/css-scope-alternative-naming-conventions.jpg"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://www.smashingmagazine.com/feed/</id>
            <title type="html">Smashing Magazine</title>
            <link href="https://www.smashingmagazine.com" rel="alternate" type="text/html"/>
            <updated>2026-03-05T06:04:47Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cba61ae76:123cf3:98d097d6</id>
        <title type="html">Your skip link targets don't need tabindex=-1 to work properly</title>
        <published>2026-03-04T19:44:49Z</published>
        <updated>2026-03-04T19:44:54Z</updated>
        <link href="https://matuzo.at/blog/2026/skip-links-tabindex" rel="alternate" type="text/html"/>
        <summary type="html">I'm a frontend developer in Graz, specialized in HTML, accessibility, and CSS layout and architecture.</summary>
        <content type="html">
&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;article&gt;&lt;h1&gt;Your skip link targets don't need tabindex=-1 to work properly&lt;/h1&gt;
&lt;p&gt;
posted on &lt;time datetime="2026-03-04"&gt;04.03.2026&lt;/time&gt;&lt;/p&gt;
&lt;p&gt;
&lt;/p&gt;
&lt;p&gt;Recently, someone posted on LinkedIn that skip links are often broken because their target elements are missing a &lt;code&gt;tabindex&lt;/code&gt; attribute. I was really surprised to see that because I thought that was an issue of the past. That's why I decided to test it.&lt;/p&gt; &lt;h2&gt;The original problem&lt;/h2&gt;
&lt;p&gt;The author explained in their post that when you use a keyboard and press Enter on a skip link, the page scrolls down to the target, but focus stays on the link. When you press Tab, focus doesn't jump to the target; it jumps to the next focusable element after the skip link. He calls that a “phantom jump”.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&amp;lt;a href=&amp;quot;#content&amp;quot;&gt;Jump to content&amp;lt;/a&gt;
&amp;lt;nav&gt;
    &amp;lt;!-- A bunch of links --&gt;
&amp;lt;/nav&gt;
&amp;lt;main id=&amp;quot;content&amp;quot;&gt;
&amp;lt;/main&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They explain that the problem is that the main element is not an interactive element and thus cannot be focused. That's why their proposed solution is adding a &lt;code&gt;tabindex&lt;/code&gt; attribute.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&amp;quot;#content&amp;quot;&gt;Jump to content&amp;lt;/a&gt;
&amp;lt;nav&gt;
    &amp;lt;!-- A bunch of links --&gt;
&amp;lt;/nav&gt;
&amp;lt;main id=&amp;quot;content&amp;quot; tabindex=&amp;quot;-1&amp;quot;&gt;
&amp;lt;/main&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What the author describes was a problem… a long time ago. As &lt;a href="https://sarahmhigley.com/writing/focus-navigation-start-point/#assistive-tech-support"&gt;Sara Higley reconstructs&lt;/a&gt;, Firefox was the first browser to fix it in 2013, followed by Internet Explorer, and finally Chrome in 2016. I don't know about Safari, but it was long enough ago that I can't remember anymore.&lt;/p&gt;
&lt;h2&gt;The solution&lt;/h2&gt;
&lt;p&gt;The fix browsers implemented is a feature called &lt;a href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"&gt;“sequential focus navigation starting point (SFNSP)”&lt;/a&gt;. Rob Dodson defines it well in his post &lt;a href="https://developer.chrome.com/blog/focus-start-point"&gt;“Remove headaches from focus management”.&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The 'sequential focus navigation starting point' feature defines where we start to search for focusable elements for sequential focus navigation (Tab or Shift-Tab) when there is no focused area.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Prior to that, there were only two possible starting points. &lt;/p&gt;
&lt;ul&gt;&lt;li&gt;If there is no other starting point, it's the &lt;code&gt;document&lt;/code&gt; or, if available, the currently active &lt;code&gt;dialog&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If an element is focused, it's also the starting point &lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;With SFNSP, there can now be more starting points.&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Navigating to a page fragment, e.g., by clicking a skip link, sets the starting point.&lt;/li&gt;
&lt;li&gt;Clicking any element on the page, interactive or not, sets the starting point.&lt;/li&gt;
&lt;li&gt;If an element that was the starting point is removed from the DOM, its parent becomes the starting point.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;That was a necessary addition because it fixed several fundamental accessibility issues. As mentioned, it's been available in all major browsers for many years and works well. Nevertheless, I want to back that with actual tests. In this blog post, I want to focus only on the scenario where navigating to a page fragment sets the starting point.&lt;/p&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;p&gt;For my test, I created a &lt;a href="https://codepen.io/matuzo/pen/pvEgaEq"&gt;simple page&lt;/a&gt;. My goal was to confirm three outcomes:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;After pressing Enter on the skip link, then pressing Tab, I would land on the “Content” button using a keyboard.&lt;/li&gt;
&lt;li&gt;After pressing Enter on the skip link, then pressing Tab, I would land on the “Content” button using a keyboard with a screen reader.&lt;/li&gt;
&lt;li&gt;After pressing Enter on the skip link, I would use the virtual cursor with a screen reader to move to the next element and land on the “Content” button or the h2.&lt;/li&gt;
&lt;/ul&gt;&lt;pre&gt;&lt;code&gt;&amp;lt;header&gt;
  &amp;lt;a href=&amp;quot;#content&amp;quot;&gt;Skip&amp;lt;/a&gt;
  &amp;lt;button&gt;1&amp;lt;/button&gt;
  &amp;lt;button&gt;2&amp;lt;/button&gt;
  &amp;lt;button&gt;3&amp;lt;/button&gt;
  &amp;lt;button&gt;4&amp;lt;/button&gt;
  &amp;lt;button&gt;5&amp;lt;/button&gt;
&amp;lt;/header&gt;

&amp;lt;main id=&amp;quot;content&amp;quot;&gt;
  &amp;lt;h2&gt;Main content&amp;lt;/h2&gt;
  &amp;lt;button&gt;Content&amp;lt;/button&gt;
&amp;lt;/main&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I was able to confirm all three scenarios in all tested browsers.&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Keyboard, Firefox 157, macOS 26.3&lt;/li&gt;
&lt;li&gt;Keyboard, Chrome 145, macOS 26.3&lt;/li&gt;
&lt;li&gt;Keyboard, Safari, macOS 26.3&lt;/li&gt;
&lt;li&gt;Keyboard Windows 11, Chrome 145&lt;/li&gt;
&lt;li&gt;Keyboard Windows 11, Edge 145&lt;/li&gt;
&lt;li&gt;Keyboard, Windows 11, Firefox 147&lt;/li&gt;
&lt;li&gt;Tab key / Virtual cursor, VoiceOver, macOS 26.3, Safari&lt;/li&gt;
&lt;li&gt;Tab key / Virtual cursor, JAWS 2026, Windows 11, Chrome 145&lt;/li&gt;
&lt;li&gt;Tab key / Virtual cursor, NVDA 2025.3.3, Windows 11, Firefox 147&lt;/li&gt;
&lt;li&gt;Tab key / Virtual cursor, NVDA 2025.3.3, Windows 11, Chrome145&lt;/li&gt;
&lt;li&gt;Tab key / Virtual cursor, Narrator, Windows 11, Edge/Chrome 145&lt;/li&gt;
&lt;li&gt;Talkback, Android 16, Chrome 145&lt;/li&gt;
&lt;/ul&gt;&lt;h2&gt;Styling&lt;/h2&gt;
&lt;p&gt;Navigating to a non-interactive page fragment doesn't reveal the element's focus styling, because it's not focused, it's only a starting point. We still have options to style a targeted element in CSS.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:target {
  outline: 2px solid #ccc;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Unless I'm missing something (please &lt;a href&gt;let me know&lt;/a&gt;), I would say it's safe to remove &lt;code&gt;tabindex=&amp;quot;-1&amp;quot;&lt;/code&gt; from your skip link targets.&lt;br&gt;
I didn't link to the post on LinkedIn because I didn't want to callout the author. It's not about them being wrong, but about the fact that browsers and screen readers receive frequent updates. That's why it's important that you reevaluate the best practices you've been following for years every now and then and check if they still apply. Often, things that were true a couple of years ago aren't true today.&lt;/p&gt; &lt;/article&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</content>
        <author>
            <name>Manuel Matuzović</name>
        </author>
        <media:content medium="image" url="https://res.cloudinary.com/dp3mem7or/image/upload/w_1200/articles/sm_target-tabindex.png?s=213"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cba36ebfe:100ae8:aed68aa9</id>
        <title type="html">Claude is an Electron App because we’ve lost native</title>
        <published>2026-03-04T18:58:07Z</published>
        <updated>2026-03-04T18:58:11Z</updated>
        <link href="https://tonsky.me/blog/fall-of-native/" rel="alternate" type="text/html"/>
        <summary type="html">Article argues that Claude is not an Electron app not because LLMs can’t do it, but because there are no advantages left for native</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;article&gt;&lt;h1&gt;Claude is an Electron App because we’ve lost native&lt;/h1&gt;
        &lt;p&gt;In &lt;a href="https://www.dbreunig.com/2026/02/21/why-is-claude-an-electron-app.html" target="_blank"&gt;“Why is Claude an Electron App?”&lt;/a&gt; Drew Breunig wonders:&lt;/p&gt;
        &lt;blockquote&gt;
          &lt;p&gt;Claude spent $20k on an agent swarm implementing (kinda) a C-compiler in Rust, but desktop Claude is an Electron app.&lt;/p&gt;
          &lt;p&gt;If code is free, why aren’t all apps native?&lt;/p&gt;
        &lt;/blockquote&gt;
        &lt;p&gt;And then argues that the answer is that LLMs are not good enough yet. They can do 90% of the work, so there’s still a substantial amount of manual polish, and thus, increased costs.&lt;/p&gt;
        &lt;p&gt;But I think that’s not the real reason. The real reason is: native has nothing to offer.&lt;/p&gt;
        &lt;p&gt;API-wise, native apps lost to web apps a long time ago. Native APIs are terrible to use, and OS vendors use everything in their power to make you not want to develop native apps for their platform. That explains the rise of Electron before LLM times, but it’s also a problem that LLMs solve now: if that was a real barrier to developing native apps, it doesn’t exist anymore.&lt;/p&gt;
        &lt;p&gt;Then there’re looks and consistency. Some time ago, maybe in the late 90s and 2000s, native was ahead. It used to look good, it was consistent, and it all actually worked: the more apps used native look and feel, the better user experience was across apps (which we used to call programs). &lt;/p&gt;
        &lt;p&gt;These days, though, native is as bad as the web, if not worse. Consistency is basically out the window. Anything can look like anything, buttons have no borders, contrast doesn’t exist, and neither do conventions. Apple, for example, seems to place traffic lights and corner radius by vibes rather than by any measurable guidelines.&lt;/p&gt;
        &lt;figure&gt;&lt;img src="https://tonsky.me/blog/fall-of-native/radii@2x.webp?t=1772581463"&gt;&lt;figcaption&gt;&lt;a href="https://x.com/vaxryy/status/1977175437382930662" target="_blank"&gt;Maybe the server should round the corners?&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Looks could be good, but they also can be bad, and then you are stuck with platform-consistent, but generally bad UI (Liquid Glass ahem). It changes too often, too: the app you made today will look out of place next year, when Apple decides to change look and feel yet again. There’s no native look anymore.&lt;/p&gt;
        &lt;figure&gt;&lt;img src="https://tonsky.me/blog/fall-of-native/run@2x.webp?t=1772581463"&gt;&lt;figcaption&gt;&lt;a href="https://grumpy.website/1723" target="_blank"&gt;Computer UIs also degrade over time&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Theoretically, native apps can integrate with OS on a deeper level. This sounds nice, but what does that mean in practice? There are almost no good interoperable file formats; everything is locked inside individual apps, most services moved to the web, and OSes dropped the ball for making a good shared baseline. You can integrate with OS-provided calendar, but you can’t do it with web calendar. Well, you can, of course, but it’s easier on the web; native doesn’t help with it at all.&lt;/p&gt;
        &lt;figure&gt;&lt;img src="https://tonsky.me/blog/fall-of-native/calendar@2x.webp?t=1772581463"&gt;&lt;figcaption&gt;&lt;a href="https://grumpy.website/1747" target="_blank"&gt;Web pages only lead to more web pages&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Finally, the last hope of people longing for native is performance. They feel that native apps will be faster. Well, they can, but it doesn’t mean they will. Web apps can be faster, too, but in practice, nobody cares. There’s no technical reason why &lt;a href="https://tonsky.me/blog/js-bloat/"&gt;Slack needs to load 80 MiB&lt;/a&gt; just to show 10 channel names and 3 messages on a screen. The web is not the problem here! It’s a choice to be bad. What makes you think it’ll be different once the company decides to move to native?&lt;/p&gt;
        &lt;p&gt;Don’t get me wrong: writing this brings me no joy. I don’t think web is a solution either. I just remember good times when native did a better-than-average job, and we were all better for using it, and it saddens me that these times have passed.&lt;/p&gt;
        &lt;p&gt;I just don’t think that kidding ourselves that the only problem with software is Electron and it all will be butterflies and unicorns once we rewrite Slack in SwiftUI is not productive. The real problem is a lack of care. And the slop; you can build it with any stack.&lt;/p&gt;
        
      &lt;/article&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Nikita Prokopov</name>
        </author>
        <media:content medium="image" url="https://dynogee.com/gen?id=nm509093bpj50lv&amp;title=Claude+is+an+Electron+App+because+we%E2%80%99ve+lost+native"/>
        <link href="https://dynogee.com/gen?id=24m2qx9uethuw6p&amp;title=Claude+is+an+Electron+App+because+we%E2%80%99ve+lost+native" rel="enclosure" type="image"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://tonsky.me/atom.xml</id>
            <title type="html">tonsky.me</title>
            <link href="https://tonsky.me" rel="alternate" type="text/html"/>
            <updated>2026-03-04T18:58:11Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cad6bfca5:b057c8:14c12a2</id>
        <title type="html">Your car’s tire sensors could be used to track you</title>
        <published>2026-03-02T07:21:01Z</published>
        <updated>2026-03-02T07:21:06Z</updated>
        <link href="https://networks.imdea.org/your-cars-tire-sensors-could-be-used-to-track-you/" rel="alternate" type="text/html"/>
        <summary type="html">Researchers at IMDEA Networks Institute, together with European partners, have found that tire pressure sensors in modern cars can unintentionally expose drivers to tracking. Over a ten-week study, they collected signals from more than 20,000 vehicles, revealing a hidden privacy risk and highlighting the need for stronger security measures in future vehicle sensor systems. Most...</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div id="single_title"&gt;

								&lt;h1&gt;Your car’s tire sensors could be used to track you&lt;/h1&gt;&lt;h4&gt;Researchers at IMDEA Networks show standard tire sensors can expose drivers’ movements, raising privacy concerns&lt;/h4&gt;								&lt;p&gt;&lt;span id="date"&gt;25 February 2026&lt;/span&gt;&lt;/p&gt;
								

																



								&lt;p&gt;&lt;span lang="EN-US"&gt;Researchers at IMDEA Networks Institute, together with European partners, have found that &lt;strong&gt;tire pressure sensors in modern cars can unintentionally expose drivers to tracking&lt;/strong&gt;. Over a ten-week study, they collected signals from more than 20,000 vehicles, revealing a hidden privacy risk and highlighting the need for stronger security measures in future vehicle sensor systems.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;Most modern cars are equipped with a &lt;strong&gt;Tire Pressure Monitoring System (TPMS)&lt;/strong&gt;, mandatory since the late 2000s in many countries for their contribution to road safety. This system uses small sensors in each wheel to monitor tire pressure and sends wireless signals to the car’s computer to alert the driver if a tire is underinflated.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;However, &lt;strong&gt;the researchers found that these tire sensors also send a unique ID number in clear, unencrypted wireless signals&lt;/strong&gt;, meaning that anyone nearby with a simple radio receiver can capture the signal, and recognize the same car again later. Most vehicle tracking today uses cameras that need clear visibility and line-of-sight to a car. TPMS tracking is different: tire sensors automatically send radio signals that pass through walls and vehicles, allowing small hidden wireless receivers to capture them without being seen. Because each sensor broadcasts a fixed unique ID, the same car can be recognized repeatedly without reading a license plate. &lt;strong&gt;This makes TPMS-based tracking cheaper, harder to detect&lt;/strong&gt;, and more difficult to avoid than camera-based surveillance, and therefore a stronger privacy threat. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;To test how serious this risk is, the team built a network of low-cost radio receivers, located near roads and parking areas. The necessary equipment costs only $100 per receiver. In total, they collected more than six million tire sensor messages from over 20,000 cars. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;“Our results show that these tire sensor signals can be used to follow vehicles and learn their movement patterns,” says &lt;strong&gt;Domenico Giustiniano&lt;/strong&gt;, Research Professor at IMDEA Networks Institute. “This means a network of inexpensive wireless receivers could quietly monitor the patterns of cars in real-world environments. Such information could reveal daily routines, such as work arrival times or travel habits.”&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;The researchers also &lt;strong&gt;developed methods to match signals from the four tires of a car&lt;/strong&gt;. This allowed them to increase the accuracy of specific vehicles arriving, living, or following regular schedules. The study showed that signals can be captured from moving cars and from distances greater than 50 meters, even when sensors are inside buildings or hidden locations. This makes covert tracking technically feasible. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;Additionally, TPMS signals include tire pressure readings, which may reveal the type of vehicle or whether a car or truck is carrying heavy loads. This could allow more advanced forms of surveillance.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;“As vehicles become increasingly connected, &lt;strong&gt;even safety-oriented sensors like TPMS should be designed with security in mind&lt;/strong&gt;, since data that appears passive and harmless can become a powerful identifier when collected at scale,” highlights &lt;strong&gt;Dr.&lt;/strong&gt; &lt;strong&gt;Alessio Scalingi&lt;/strong&gt;, former PhD student at IMDEA Networks and now Assistant Professor at UC3M, Madrid.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;Despite these risks, current vehicle cybersecurity regulations do not yet specifically address TPMS security. The researchers warn that without encryption or authentication, tire sensors remain an easy target for passive surveillance.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;“TPMS was designed for safety, not security,” adds &lt;strong&gt;Dr. Yago Lizarribar&lt;/strong&gt;, former PhD student at IMDEA Networks during the research study, and now Researcher at Armasuisse, Switzerland. “&lt;strong&gt;Our findings show the need for manufacturers and regulators to improve protection&lt;/strong&gt; in future vehicle sensor systems.”&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;Therefore, the research team urges the manufacturers and policymakers to strengthen cybersecurity in future cars, so that safety systems do not become tools for tracking the population. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;The paper, titled “&lt;a href="https://dspace.networks.imdea.org/handle/20.500.12761/2011" target="_blank" rel="noopener"&gt;Can’t Hide Your Stride: Inferring Car Movement Patterns from Passive TPMS Measurements&lt;/a&gt;,” has been accepted for publication at IEEE WONS 2026.&lt;/span&gt;&lt;/p&gt;



								&lt;div id="taxonomy"&gt;

									&lt;p&gt;Source(s): &lt;span&gt;IMDEA Networks Institute&lt;/span&gt;&lt;br&gt;&lt;/p&gt;

									
									
								&lt;/div&gt;
								
								
							&lt;/div&gt;

						
					&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Marta Dorado</name>
        </author>
        <media:content medium="image" url="https://networks.imdea.org/wp-content/uploads/2026/02/media-file-parking-lot-1024x576.png"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19ca0820f80:1a9cfd:f46e742f</id>
        <title type="html">Please, please, please stop using passkeys for encrypting user data</title>
        <published>2026-02-27T19:10:04Z</published>
        <updated>2026-02-27T19:10:07Z</updated>
        <link href="https://blog.timcappalli.me/p/passkeys-prf-warning/" rel="alternate" type="text/html"/>
        <summary type="html">Passkeys are the future of authentication, but using them for data encryption is a disaster waiting to happen. Overloading these credentials creates a dangerous blast radius that can lead to the irreversible loss of a user's most sacred memories and documents.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;Why am I writing this today?
Because I am deeply concerned about users losing their most sacred data.&lt;/p&gt;&lt;p&gt;Over the past year or two, I’ve seen many organizations, large and small, implement passkeys (which is great, thank you!) and use the PRF (Pseudo-Random Function) extension to derive keys to protect user data, typically to support end-to-end encryption (including backups).
I’ve also seen a number of influential folks and organizations promote the use of PRF for encrypting data.&lt;/p&gt;&lt;p&gt;The primary use cases I’ve seen implemented or promoted so far include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;encrypting message backups (including images and videos)&lt;/li&gt;&lt;li&gt;end-to-end encryption&lt;/li&gt;&lt;li&gt;encrypting documents and other files&lt;/li&gt;&lt;li&gt;encrypting and unlocking crypto wallets&lt;/li&gt;&lt;li&gt;credential manager unlocking&lt;/li&gt;&lt;li&gt;local account sign in&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Why is this a problem?&lt;/p&gt;&lt;p&gt;When you overload a credential used for authentication by also using it for encryption, the “blast radius” for losing that credential becomes immeasurably larger.&lt;/p&gt;&lt;p&gt;Imagine a user named Erika. They are asked to set up encrypted backups in their favorite messaging app because they don’t want to lose their messages and photos, especially those of loved ones who are no longer here.
Erika is prompted to use their passkey to enable these backups.&lt;/p&gt;&lt;p&gt;There is nothing in the UI that emphasizes that these backups are now tightly coupled to their passkey. Even if there were explanatory text, Erika, like most users, doesn’t typically read through every dialog box, and they certainly can’t be expected to remember this technical detail a year from now.&lt;/p&gt;&lt;p&gt;A few months pass, and Erika decides to clean up their credential manager. They don’t remember why they had a specific passkey for a messaging app and deletes it.&lt;/p&gt;&lt;p&gt;Fast forward a year: they get a new phone and set up the messaging app. They aren’t prompted to use a passkey because one no longer exists in their credential manager. Instead, they use phone number verification to recover their account. They are then guided through the “restore backup” flow and prompted for their passkey.&lt;/p&gt;&lt;p&gt;Since they no longer have it, they are informed that they cannot access their backed up data. Goodbye, memories.&lt;/p&gt;&lt;p&gt;Here’s a few examples of what a user sees when they delete a passkey:&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt="Apple Passwords delete passkey" src="https://blog.timcappalli.me/p/passkeys-prf-warning/delete-ap_hu_2f537039f66a265f.png"&gt;&lt;figcaption&gt;Example: deleting a passkey in Apple Passwords&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt="Google Password Manager delete passkey" src="https://blog.timcappalli.me/p/passkeys-prf-warning/delete-gpm_hu_999e554d404d2d82.png"&gt;&lt;figcaption&gt;Example: deleting a passkey in Google Password Manager&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt="Bitwarden delete passkey" src="https://blog.timcappalli.me/p/passkeys-prf-warning/delete-bw_hu_ad31ff008fd77df7.png"&gt;&lt;figcaption&gt;Example: deleting a passkey in Bitwarden&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;How is a user supposed to understand that they are potentially blowing away photos of deceased relatives, an encrypted property deed, or their digital currency?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;We cannot, and should not, expect users to know this.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;At this point, you may be asking why PRF is part of WebAuthn in the first place.
There are some very legitimate and more durable uses of PRF in WebAuthn, specifically supporting credential managers and operating systems.&lt;/p&gt;&lt;p&gt;A passkey with PRF can make unlocking your credential manager (where all of your other passkeys and credentials are stored) much faster and more secure.
Credential managers have robust mechanisms to protect your vault data with multiple methods, such as master passwords, per-device keys, recovery keys, and social recovery keys.
Losing access to a passkey used to unlock your credential manager rarely leads to complete loss of your vault data.&lt;/p&gt;&lt;p&gt;PRF is already implemented in WebAuthn Clients and Credential Managers, so the cat is out of the bag. My asks:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;To the wider identity industry&lt;/strong&gt;: &lt;strong&gt;&lt;em&gt;please stop promoting and using passkeys to encrypt user data. I’m begging you. Let them be great, phishing-resistant authentication credentials.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;To &lt;strong&gt;credential managers&lt;/strong&gt;: please prioritize adding warnings for users when they delete a passkey with PRF (and &lt;a href="https://www.w3.org/TR/passkey-endpoints/#usage" target="_blank" rel="noreferrer"&gt;displaying the RP’s info page when available&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;To &lt;strong&gt;sites and services using passkeys&lt;/strong&gt;: if you still need to use PRF knowing these concerns, please:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;add an informational page to your support site explaining how you’re using passkeys for more than authentication&lt;/li&gt;&lt;li&gt;list that page URL in the &lt;a href="https://www.w3.org/TR/passkey-endpoints/#usage" target="_blank" rel="noreferrer"&gt;Well-Known URL for Relying Party Passkey Endpoints (&lt;code&gt;prfUsageDetails&lt;/code&gt;)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;provide as much warning as possible up front to users when enabling it&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Thanks for reading! &#128591;&#127995;&lt;/p&gt;&lt;p&gt;(and thanks to &lt;a href="https://blog.millerti.me/" target="_blank" rel="noreferrer"&gt;Matthew Miller&lt;/a&gt; for reviewing and providing feedback on this post)&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Tim Cappalli</name>
        </author>
        <media:content medium="image" url="https://blog.timcappalli.me/p/passkeys-prf-warning/featured.jpg"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19c94728918:7e51:7c94cf89</id>
        <title type="html">How &amp;quot;Liquid Design&amp;quot; Broke the iPhone and Forced Apple’s Great Reset</title>
        <published>2026-02-25T10:57:40Z</published>
        <updated>2026-02-25T10:57:43Z</updated>
        <link href="https://webdesignerdepot.com/how-liquid-design-broke-the-iphone-and-forced-apples-great-reset/" rel="alternate" type="text/html"/>
        <summary type="html">Apple’s &amp;quot;Liquid Glass&amp;quot; experiment has officially shattered, proving that obsession with aesthetics over usability is a billion-dollar mistake. As iOS 26 drains batteries and kills accessibility, a massive internal &amp;quot;Solid Design&amp;quot; rescue mission is underway to save the iPhone from its own ego.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;The launch of &lt;strong&gt;iOS 26&lt;/strong&gt; was heralded as a “Spatial Awakening.” Apple’s design team, led by the high-concept vision of Alan Dye, promised to bridge the gap between our physical reality and our digital tools through a language called &lt;strong&gt;Liquid Glass&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;It was marketed as an interface that breathes—a hyper-dynamic, translucent, and motion-heavy UI that used real-time refraction to make your apps look like they were floating in a physical pane of crystal.&lt;/p&gt;&lt;p&gt;But six months into the lifecycle of iOS 26, the verdict is in from power users, developers, and accessibility advocates alike: &lt;strong&gt;Liquid Glass is a usability catastrophe.&lt;/strong&gt; &lt;/p&gt;&lt;p&gt;It is the most arrogant piece of software Apple has ever shipped—a system that demands you admire its beauty while it actively hinders your ability to use your phone. It is a house of mirrors built on the bones of a once-efficient operating system, and its failure has forced the most radical leadership shakeup in Cupertino since the departure of Jony Ive.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;The Death of Legibility: A Low-Vision Nightmare&lt;/h2&gt;&lt;p&gt;The most fundamental job of a user interface is to be readable. Liquid Glass fails this at a level that would get a first-year design student expelled. By prioritizing “refractive realism,” Apple replaced solid containers with semi-transparent, light-bending “panes.”&lt;/p&gt;&lt;p&gt;The problem is that the real world is messy. When your notification center is refracting a high-detail photo of a forest or a bright sunset, the text “loses the fight” for your attention. This has created a &lt;strong&gt;Contrast Crisis&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Apple’s AI tries to dynamically shift text color based on the background, but it frequently fails on busy images, leading to a “muddy” effect where text becomes functionally invisible.&lt;/p&gt;&lt;p&gt;For users with visual impairments—or even just tired eyes—the constant shifting of translucent layers creates a barrier to entry. We are now in an era where “Reduce Transparency” isn’t just an accessibility option; it is a mandatory setting for anyone who wants to read their messages in direct sunlight.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;The Performance Penalty: GPU-Driven Ego&lt;/h2&gt;&lt;p&gt;Liquid Glass treats the iPhone UI like a AAA video game. Every time you swipe, the system calculates real-time ray-traced reflections and “chromatic aberration” on the edges of your app icons. It is a staggering waste of compute power that has introduced a level of friction we haven’t seen in years.&lt;/p&gt;&lt;p&gt;Even on the powerhouse &lt;strong&gt;iPhone 17 Pro&lt;/strong&gt;, the interface suffers from “micro-stutters.” The moment the GPU throttles due to heat or background tasks, the “liquid” illusion breaks, leaving the user with a laggy, disconnected experience.&lt;/p&gt;&lt;p&gt;Furthermore, the &lt;strong&gt;Battery Tax&lt;/strong&gt; has been devastating. Data suggests a 10–15% reduction in real-world screen-on time because the phone is effectively running a physics engine just to show you your calendar. We are using 3-nanometer chips—miracles of human engineering—to render “shimmers” on icons that were perfectly functional as flat squares.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;The Uncanny Valley of Physics&lt;/h2&gt;&lt;p&gt;In 2013, iOS 7 killed skeuomorphism because we no longer needed digital objects to look like leather or felt. Liquid Glass is a return to a different kind of realism—&lt;strong&gt;Digital Skeuomorphism&lt;/strong&gt;. Apple tried to make us believe we are touching floating panes of glass, but when the physics don’t match the tactile reality, it falls into an uncanny valley.&lt;/p&gt;&lt;p&gt;The “wobbly” sliders and “floaty” icons lack the weight and intent that made the original iPhone feel like a precision tool. When you move a slider in the Control Center and it stretches like digital gelatin, it feels flimsy and unserious.&lt;/p&gt;&lt;p&gt;It is “Barbie-fied” tech—glossy, plastic, and fundamentally performative. As critic John Gruber noted, Apple spent billions making the thinnest hardware in the world only to put a UI on it that looks like a Windows Vista fever dream.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;The Alan Dye Departure and the Internal Revolt&lt;/h2&gt;&lt;p&gt;The departure of &lt;strong&gt;Alan Dye&lt;/strong&gt; to Meta in late 2025 was the ultimate confirmation that the “Liquid” experiment had reached its dead end.&lt;/p&gt;&lt;p&gt;Reports from within Apple Park suggest that Dye’s team frequently brushed aside warnings from accessibility and performance engineers to maintain the “purity” of the aesthetic.&lt;/p&gt;&lt;p&gt;When Dye left, he left behind a UI that looked stunning in a 4K keynote presentation but felt “restless and needy” in the hands of a real person. The fact that Apple had to rush out &lt;strong&gt;iOS 26.2&lt;/strong&gt; with an “Opaque Mode” was a silent admission of failure.&lt;/p&gt;&lt;p&gt;Apple doesn’t add “undo” sliders to its core vision unless that vision is actively breaking the user experience.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;The Rescue Mission: Stephen Lemay and “Solid Design”&lt;/h2&gt;&lt;p&gt;The keys to the iPhone’s soul have now been handed to &lt;strong&gt;Stephen Lemay&lt;/strong&gt;, a 26-year veteran of the original Aqua design era. Lemay’s mission for &lt;strong&gt;iOS 27&lt;/strong&gt; is reportedly a “Snow Leopard” style intervention—a radical “taming” of the interface focused on three pillars:&lt;/p&gt;&lt;ol start="1" class="wp-block-list"&gt;&lt;li&gt;&lt;strong&gt;The Return of Opacity:&lt;/strong&gt; Moving away from high-refraction backgrounds to solid, high-contrast containers that prioritize legibility.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;GPU Idle Recovery:&lt;/strong&gt; Targeting a 40% reduction in baseline GPU activity by stripping away real-time physics from static screens.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Edge Definition:&lt;/strong&gt; Reintroducing high-contrast borders and “Physical Depth” to ensure the eye never has to “hunt” for a button.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The shift under Lemay is philosophical. Under Dye, Apple design became performative—it wanted you to look &lt;em&gt;at&lt;/em&gt; the glass. Under Lemay, the goal is for the design to disappear again.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;Conclusion: Form Follows Nothing&lt;/h2&gt;&lt;p&gt;Liquid Glass failed because it forgot why people buy iPhones. We don’t buy them to look at a digital art gallery; we buy them to get things done. By turning the interface into an obstacle course of reflections and blurs, Apple traded &lt;strong&gt;utility&lt;/strong&gt; for &lt;strong&gt;vanity&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;It was a beautiful mistake, a technical marvel that served no master other than the ego of the designers who built it.&lt;/p&gt;&lt;p&gt;As we look toward the “Solid Design” of iOS 27, Liquid Glass will likely be remembered alongside the “butterfly keyboard”—a time when Apple got so caught up in &lt;em&gt;how&lt;/em&gt; they could build something, they forgot to ask &lt;em&gt;if&lt;/em&gt; they should.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;Noah Davis is an accomplished UX strategist with a knack for blending innovative design with business strategy. With over a decade of experience, he excels at crafting user-centered solutions that drive engagement and achieve measurable results.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Noah Davis</name>
        </author>
        <media:content medium="image" url="https://d3tamksjp7q04h.cloudfront.net/2026/02/13144445/Screenshot-2026-02-13-at-11.44.24-AM.jpeg"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://webdesignerdepot.com/feed/</id>
            <title type="html">Web Designer Depot</title>
            <link href="https://webdesignerdepot.com" rel="alternate" type="text/html"/>
            <updated>2026-02-25T10:57:43Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19c946c2b85:31b24:c24964f6</id>
        <title type="html">Precompressed HTML at the Edge: Eleventy Meets Cloudflare Workers</title>
        <published>2026-02-25T10:50:42Z</published>
        <updated>2026-02-25T10:50:47Z</updated>
        <link href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/" rel="alternate" type="text/html"/>
        <summary type="html">In this post, I will show you how I integrate Brotli level 11 compression directly into my 11ty build process to squeeze every possible byte out of my blog’s HTML.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#introduction"&gt;&lt;span&gt;Jump to section titled: Introduction&lt;/span&gt;&lt;/a&gt;&lt;p&gt;In 2025, I wrote a series of web performance optimisation blog posts focussing on some of the key fundamental's of Frontend Web Performance:&lt;/p&gt;&lt;h3 id="caching"&gt;Caching&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#caching"&gt;&lt;span&gt;Jump to section titled: Caching&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;a href="https://nooshu.com/blog/2025/09/02/asset-fingerprinting-and-the-preload-response-header-in-11ty/"&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="compression"&gt;Compression&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#compression"&gt;&lt;span&gt;Jump to section titled: Compression&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;a href="https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/"&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="concatenation"&gt;Concatenation&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#concatenation"&gt;&lt;span&gt;Jump to section titled: Concatenation&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;a href="https://nooshu.com/blog/2025/01/12/using-an-11ty-shortcode-to-craft-a-custom-css-pipeline/"&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="https://www.npmjs.com/package/clean-css"&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="more-compression"&gt;More compression&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#more-compression"&gt;&lt;span&gt;Jump to section titled: More compression&lt;/span&gt;&lt;/a&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="why-html-brotli-matters"&gt;Why HTML Brotli matters?&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#why-html-brotli-matters"&gt;&lt;span&gt;Jump to section titled: Why HTML Brotli matters?&lt;/span&gt;&lt;/a&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="improved-web-performance"&gt;Improved Web Performance&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#improved-web-performance"&gt;&lt;span&gt;Jump to section titled: Improved Web Performance&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Every kilobyte saved here multiplies across your traffic volume.&lt;/p&gt;&lt;h3 id="at-scale-cost-savings-add-up"&gt;At scale, cost savings add up&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#at-scale-cost-savings-add-up"&gt;&lt;span&gt;Jump to section titled: At scale, cost savings add up&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Let’s assume you are serving a high traffic website, say the &lt;a href="https://www.bbc.co.uk"&gt;BBC&lt;/a&gt;, &lt;a href="https://www.google.com"&gt;Google&lt;/a&gt;, or &lt;a href="https://www.gov.uk"&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="https://www.cloudflare.com/en-gb/learning/cloud/what-are-data-egress-fees/"&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="improved-performance-on-slow-and-unstable-mobile-networks"&gt;Improved performance on slow and unstable mobile networks&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#improved-performance-on-slow-and-unstable-mobile-networks"&gt;&lt;span&gt;Jump to section titled: Improved performance on slow and unstable mobile networks&lt;/span&gt;&lt;/a&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="it-indirectly-improves-core-web-vitals"&gt;It indirectly improves Core Web Vitals&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#it-indirectly-improves-core-web-vitals"&gt;&lt;span&gt;Jump to section titled: It indirectly improves Core Web Vitals&lt;/span&gt;&lt;/a&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="compression-cost-is-paid-once-for-static-assets"&gt;Compression cost is paid once for static assets&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#compression-cost-is-paid-once-for-static-assets"&gt;&lt;span&gt;Jump to section titled: Compression cost is paid once for static assets&lt;/span&gt;&lt;/a&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="compression-size-examples-v1"&gt;Compression Size Examples v1&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#compression-size-examples-v1"&gt;&lt;span&gt;Jump to section titled: Compression Size Examples v1&lt;/span&gt;&lt;/a&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="https://www.w3.org/TR/2011/WD-html5-20110405/Overview.html"&gt;W3C HTML5 Specification page&lt;/a&gt; OR &lt;a href="https://apod.nasa.gov/apod/archivepix.html"&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="w3c-html5-specification-single-page"&gt;W3C HTML5 Specification (Single Page)&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#w3c-html5-specification-single-page"&gt;&lt;span&gt;Jump to section titled: W3C HTML5 Specification (Single Page)&lt;/span&gt;&lt;/a&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 &#128557;&lt;/p&gt;&lt;h4 id="nasa-s-astronomy-picture-of-the-day-archive"&gt;Nasa’s Astronomy Picture of the Day Archive&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#nasa-s-astronomy-picture-of-the-day-archive"&gt;&lt;span&gt;Jump to section titled: Nasa’s Astronomy Picture of the Day Archive&lt;/span&gt;&lt;/a&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="how-is-this-different-from-my-other-compression-posts"&gt;How is this different from my other compression posts?&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#how-is-this-different-from-my-other-compression-posts"&gt;&lt;span&gt;Jump to section titled: How is this different from my other compression posts?&lt;/span&gt;&lt;/a&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="https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/"&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's "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="https://developers.cloudflare.com/pages/functions/"&gt;Cloudflare Pages Functions&lt;/a&gt;, which run on &lt;a href="https://workers.cloudflare.com/"&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="what-s-the-point"&gt;What’s the point?&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#what-s-the-point"&gt;&lt;span&gt;Jump to section titled: What’s the point?&lt;/span&gt;&lt;/a&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="compression-size-examples-v2"&gt;Compression Size Examples v2&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#compression-size-examples-v2"&gt;&lt;span&gt;Jump to section titled: Compression Size Examples v2&lt;/span&gt;&lt;/a&gt;&lt;h4 id="w3c-html5-specification-single-page-2"&gt;W3C HTML5 Specification (Single Page)&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#w3c-html5-specification-single-page-2"&gt;&lt;span&gt;Jump to section titled: W3C HTML5 Specification (Single Page)&lt;/span&gt;&lt;/a&gt;&lt;h4 id="nasa-s-astronomy-picture-of-the-day-archive-2"&gt;Nasa’s Astronomy Picture of the Day Archive&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#nasa-s-astronomy-picture-of-the-day-archive-2"&gt;&lt;span&gt;Jump to section titled: Nasa’s Astronomy Picture of the Day Archive&lt;/span&gt;&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href="https://apod.nasa.gov/apod/archivepix.html"&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="https://paulcalvano.com/"&gt;Paul Calvano's&lt;/a&gt; fantastic &lt;a href="https://tools.paulcalvano.com/compression-tester/"&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="problems"&gt;Problems&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#problems"&gt;&lt;span&gt;Jump to section titled: Problems&lt;/span&gt;&lt;/a&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="problem-1-url-path-vs-actual-file-path-mismatch"&gt;Problem 1: URL Path vs Actual File Path Mismatch&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#problem-1-url-path-vs-actual-file-path-mismatch"&gt;&lt;span&gt;Jump to section titled: Problem 1: URL Path vs Actual File Path Mismatch&lt;/span&gt;&lt;/a&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="https://en.wikipedia.org/wiki/Dieter_Rams"&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="problem-2-a-page-full-of-wingdings"&gt;Problem 2: A page full of Wingdings&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#problem-2-a-page-full-of-wingdings"&gt;&lt;span&gt;Jump to section titled: Problem 2: A page full of Wingdings&lt;/span&gt;&lt;/a&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="https://en.wikipedia.org/wiki/Windows_3.1"&gt;3.1&lt;/a&gt;), it came bundled with a font called &lt;a href="https://en.wikipedia.org/wiki/Wingdings"&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;amp; &lt;code&gt;¯\_(ツ)_/¯&lt;/code&gt;, which in turn would progress to the modern world of &lt;a href="https://emojipedia.org/"&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 ('charset') 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;source type="image/webp"&gt;&lt;img alt="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" src="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/so0HRc7rVz-300.jpeg"&gt;&lt;/source&gt;&lt;/p&gt;&lt;h2 id="my-approach"&gt;My Approach&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#my-approach"&gt;&lt;span&gt;Jump to section titled: My Approach&lt;/span&gt;&lt;/a&gt;&lt;h3 id="step-1-build-time-compression"&gt;Step 1: Build-time compression&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#step-1-build-time-compression"&gt;&lt;span&gt;Jump to section titled: Step 1: Build-time compression&lt;/span&gt;&lt;/a&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="step-2-cloudflare-pages-function-for-content-negotiation"&gt;Step 2: Cloudflare Pages Function for content negotiation&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#step-2-cloudflare-pages-function-for-content-negotiation"&gt;&lt;span&gt;Jump to section titled: Step 2: Cloudflare Pages Function for content negotiation&lt;/span&gt;&lt;/a&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="step-3-the-eleventy-build-uses-node-js-zlib-for-seamless-integration"&gt;Step 3: The Eleventy build uses Node.js zlib, for seamless integration&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#step-3-the-eleventy-build-uses-node-js-zlib-for-seamless-integration"&gt;&lt;span&gt;Jump to section titled: Step 3: The Eleventy build uses Node.js zlib, for seamless integration&lt;/span&gt;&lt;/a&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="https://nodejs.org/dist/latest/docs/api/zlib.html#zlib"&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="implementation"&gt;Implementation&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#implementation"&gt;&lt;span&gt;Jump to section titled: Implementation&lt;/span&gt;&lt;/a&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="core-compression-utility"&gt;Core compression utility&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#core-compression-utility"&gt;&lt;span&gt;Jump to section titled: Core compression utility&lt;/span&gt;&lt;/a&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 tabindex="0"&gt;&lt;code&gt;

&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; brotliCompressSync &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'zlib'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;


&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; &lt;span&gt;BROTLI_LEVEL&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;11&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;


&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;brotliCompress&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;input&lt;span&gt;,&lt;/span&gt; level &lt;span&gt;=&lt;/span&gt; &lt;span&gt;BROTLI_LEVEL&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; buffer &lt;span&gt;=&lt;/span&gt; Buffer&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isBuffer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;input&lt;span&gt;)&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; input &lt;span&gt;:&lt;/span&gt; Buffer&lt;span&gt;.&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;input&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;brotliCompressSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;buffer&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; level &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;/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="https://gist.github.com/Nooshu/0dd55a4ba67da0a6d0053dc0e2884ba8"&gt;this Gist&lt;/a&gt; with minimal comments and improved readability.&lt;/p&gt;&lt;h3 id="html-compression-post-build"&gt;HTML compression post-build&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#html-compression-post-build"&gt;&lt;span&gt;Jump to section titled: HTML compression post-build&lt;/span&gt;&lt;/a&gt;&lt;p&gt;The post-build HTML compression file:&lt;/p&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;

&lt;span&gt;import&lt;/span&gt; fs &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'fs'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; path &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'path'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; brotliCompress&lt;span&gt;,&lt;/span&gt; &lt;span&gt;BROTLI_LEVEL&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'./compression.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;


&lt;span&gt;function&lt;/span&gt; &lt;span&gt;findHtmlFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;dir&lt;span&gt;,&lt;/span&gt; acc &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;const&lt;/span&gt; entries &lt;span&gt;=&lt;/span&gt; fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;readdirSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;dir&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;withFileTypes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; entry &lt;span&gt;of&lt;/span&gt; entries&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;const&lt;/span&gt; fullPath &lt;span&gt;=&lt;/span&gt; path&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;dir&lt;span&gt;,&lt;/span&gt; entry&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;const&lt;/span&gt; relPath &lt;span&gt;=&lt;/span&gt; path&lt;span&gt;.&lt;/span&gt;&lt;span&gt;relative&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'./_site'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; fullPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;entry&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isDirectory&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;findHtmlFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;fullPath&lt;span&gt;,&lt;/span&gt; acc&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;entry&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; entry&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;.&lt;/span&gt;&lt;span&gt;endsWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'.html'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			acc&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;relPath&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;return&lt;/span&gt; acc&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;


&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;compressHtmlFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; startTime &lt;span&gt;=&lt;/span&gt; Date&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'&#128640; PRODUCTION POSTBUILD: Starting HTML Brotli compression'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;const&lt;/span&gt; siteDir &lt;span&gt;=&lt;/span&gt; path&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'./_site'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;existsSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;siteDir&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'⚠️  _site directory not found, skipping HTML compression'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	&lt;span&gt;const&lt;/span&gt; htmlFiles &lt;span&gt;=&lt;/span&gt; &lt;span&gt;findHtmlFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;siteDir&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;htmlFiles&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'⚠️  No HTML files found, skipping compression'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	&lt;span&gt;let&lt;/span&gt; compressedCount &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;let&lt;/span&gt; skippedCount &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;let&lt;/span&gt; totalOriginal &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;let&lt;/span&gt; totalCompressed &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; errors &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;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; relPath &lt;span&gt;of&lt;/span&gt; htmlFiles&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;const&lt;/span&gt; inputPath &lt;span&gt;=&lt;/span&gt; path&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;siteDir&lt;span&gt;,&lt;/span&gt; relPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;const&lt;/span&gt; outputPath &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;inputPath&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.br&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

		&lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			
			&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;existsSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;outputPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
				&lt;span&gt;const&lt;/span&gt; inputStats &lt;span&gt;=&lt;/span&gt; fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;statSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;inputPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				&lt;span&gt;const&lt;/span&gt; outputStats &lt;span&gt;=&lt;/span&gt; fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;statSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;outputPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;outputStats&lt;span&gt;.&lt;/span&gt;mtime &lt;span&gt;&amp;gt;=&lt;/span&gt; inputStats&lt;span&gt;.&lt;/span&gt;mtime&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
					skippedCount&lt;span&gt;++&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
					&lt;span&gt;continue&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				&lt;span&gt;}&lt;/span&gt;
			&lt;span&gt;}&lt;/span&gt;

			&lt;span&gt;const&lt;/span&gt; fileContent &lt;span&gt;=&lt;/span&gt; fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;readFileSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;inputPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;const&lt;/span&gt; originalSize &lt;span&gt;=&lt;/span&gt; fileContent&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;;&lt;/span&gt;

			&lt;span&gt;const&lt;/span&gt; brotliBuffer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;brotliCompress&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;fileContent&lt;span&gt;,&lt;/span&gt; &lt;span&gt;BROTLI_LEVEL&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;const&lt;/span&gt; compressedSize &lt;span&gt;=&lt;/span&gt; brotliBuffer&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;;&lt;/span&gt;

			fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;writeFileSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;outputPath&lt;span&gt;,&lt;/span&gt; brotliBuffer&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			compressedCount&lt;span&gt;++&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			totalOriginal &lt;span&gt;+=&lt;/span&gt; originalSize&lt;span&gt;;&lt;/span&gt;
			totalCompressed &lt;span&gt;+=&lt;/span&gt; compressedSize&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			errors&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&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;relPath&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;error&lt;span&gt;.&lt;/span&gt;message&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;span&gt;}&lt;/span&gt;

	&lt;span&gt;const&lt;/span&gt; totalTime &lt;span&gt;=&lt;/span&gt; Date&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;-&lt;/span&gt; startTime&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; savedPercent &lt;span&gt;=&lt;/span&gt; totalOriginal &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt; &lt;span&gt;-&lt;/span&gt; totalCompressed &lt;span&gt;/&lt;/span&gt; totalOriginal&lt;span&gt;)&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;100&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;'0'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;✅ Compressed &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;compressedCount&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; HTML file(s) (&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;skippedCount&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; skipped, up-to-date)&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;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;compressedCount &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&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;/span&gt;totalOriginal &lt;span&gt;/&lt;/span&gt; &lt;span&gt;1024&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; KB → &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;totalCompressed &lt;span&gt;/&lt;/span&gt; &lt;span&gt;1024&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; KB (&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;savedPercent&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;% smaller)&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;
	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;   Finished in &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;totalTime&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;ms (&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;totalTime &lt;span&gt;/&lt;/span&gt; &lt;span&gt;1000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;s)&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;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;errors&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'\nErrors:'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		errors&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;e&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&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;e&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;span&gt;}&lt;/span&gt;

	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&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="https://gist.github.com/Nooshu/568f44fe571d11660bc3e4e281f6329b"&gt;Gist here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id="cloudflare-pages-function-for-content-negotiation"&gt;Cloudflare Pages Function for content negotiation&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#cloudflare-pages-function-for-content-negotiation"&gt;&lt;span&gt;Jump to section titled: Cloudflare Pages Function for content negotiation&lt;/span&gt;&lt;/a&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 tabindex="0"&gt;&lt;code&gt;

&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;handleHtmlWithBrotli&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;span&gt;,&lt;/span&gt; env&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; url &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;URL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;.&lt;/span&gt;url&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; pathname &lt;span&gt;=&lt;/span&gt; url&lt;span&gt;.&lt;/span&gt;pathname&lt;span&gt;;&lt;/span&gt;

	
	
	
	
	&lt;span&gt;const&lt;/span&gt; isHtmlDocument &lt;span&gt;=&lt;/span&gt; pathname &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'/'&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; pathname&lt;span&gt;.&lt;/span&gt;&lt;span&gt;endsWith&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; pathname &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'/404.html'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;isHtmlDocument&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt; env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ASSETS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	&lt;span&gt;const&lt;/span&gt; acceptsBrotli &lt;span&gt;=&lt;/span&gt; request&lt;span&gt;.&lt;/span&gt;headers&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'Accept-Encoding'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'br'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;??&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;acceptsBrotli&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt; env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ASSETS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	
	
	
	&lt;span&gt;const&lt;/span&gt; brPath &lt;span&gt;=&lt;/span&gt;
		pathname &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'/'&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;'/index.html.br'&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; pathname &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'/404.html'&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;'/404.html.br'&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;pathname&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;index.html.br&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;const&lt;/span&gt; brUrl &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;URL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;brPath&lt;span&gt;,&lt;/span&gt; url&lt;span&gt;.&lt;/span&gt;origin&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; brResponse &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ASSETS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;brUrl&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toString&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;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;brResponse&lt;span&gt;.&lt;/span&gt;ok&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt; env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ASSETS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	
	
	&lt;span&gt;const&lt;/span&gt; headers &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Headers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;brResponse&lt;span&gt;.&lt;/span&gt;headers&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	headers&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'Content-Encoding'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;'br'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	headers&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'Content-Type'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;'text/html; charset=UTF-8'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	headers&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'Vary'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;'Accept-Encoding'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	
	headers&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'Cache-Control'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;'public, max-age=31536000, no-transform'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;brResponse&lt;span&gt;.&lt;/span&gt;body&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; brResponse&lt;span&gt;.&lt;/span&gt;status&lt;span&gt;,&lt;/span&gt;
		headers&lt;span&gt;,&lt;/span&gt;
		
		
		&lt;span&gt;encodeBody&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;'manual'&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;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; &lt;span&gt;onRequestGet&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt; request&lt;span&gt;,&lt;/span&gt; env &lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;handleHtmlWithBrotli&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;,&lt;/span&gt; env&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;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; &lt;span&gt;onRequestHead&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt; request&lt;span&gt;,&lt;/span&gt; env &lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;handleHtmlWithBrotli&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;,&lt;/span&gt; env&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;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with minimal comments and much better readability is available in a &lt;a href="https://gist.github.com/Nooshu/5176f0d3446e05629501130912f09570"&gt;Gist here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id="build-lifecycle-wiring"&gt;Build lifecycle wiring&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#build-lifecycle-wiring"&gt;&lt;span&gt;Jump to section titled: Build lifecycle wiring&lt;/span&gt;&lt;/a&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 tabindex="0"&gt;&lt;code&gt;

&lt;span&gt;import&lt;/span&gt; env &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_data/env.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; clearCssBuildCache &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_helpers/css-manipulation.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; generatePreloadHeaders &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_helpers/header-generator.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; compressHtmlFiles &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_helpers/html-compression.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; compressJavaScriptFiles &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_helpers/js-compression.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; minifyJavaScriptFiles &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_helpers/js-minify.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;


&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;registerBuildEvents&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;eleventyConfig&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;env&lt;span&gt;.&lt;/span&gt;isLocal&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	
	
	eleventyConfig&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'eleventy.before'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;clearCssBuildCache&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;/span&gt;

	eleventyConfig&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'eleventy.after'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'\n═══════════════════════════════════════════════════════════════════════════════'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'&#128640; PRODUCTION POSTBUILD PHASE: Beginning postbuild operations'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'═══════════════════════════════════════════════════════════════════════════════\n'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;generatePreloadHeaders&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;await&lt;/span&gt; &lt;span&gt;minifyJavaScriptFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;compressJavaScriptFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;compressHtmlFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'\n═══════════════════════════════════════════════════════════════════════════════'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'✅ PRODUCTION POSTBUILD PHASE: All postbuild operations completed'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'═══════════════════════════════════════════════════════════════════════════════\n'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

		
		
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'&#128295; Forcing cleanup of HTTP connections and timers...'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;await&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;resolve&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;resolve&lt;span&gt;,&lt;/span&gt; &lt;span&gt;100&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

		&lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			&lt;span&gt;const&lt;/span&gt; http &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;import&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'http'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;const&lt;/span&gt; https &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;import&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'https'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

			&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;http&lt;span&gt;.&lt;/span&gt;globalAgent&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
				http&lt;span&gt;.&lt;/span&gt;globalAgent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;destroy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'✅ Destroyed HTTP global agent'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;}&lt;/span&gt;
			&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;https&lt;span&gt;.&lt;/span&gt;globalAgent&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
				https&lt;span&gt;.&lt;/span&gt;globalAgent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;destroy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'✅ Destroyed HTTPS global agent'&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;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'⚠️  Could not destroy HTTP agents:'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;.&lt;/span&gt;message&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt;

		&lt;span&gt;await&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;resolve&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;resolve&lt;span&gt;,&lt;/span&gt; &lt;span&gt;50&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'✅ HTTP connection cleanup completed'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

		
		
		&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;env&lt;span&gt;.&lt;/span&gt;isProd&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'&#127937; Production build complete - forcing process exit in 2 seconds...'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
				console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'&#128075; Forcing clean exit now'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				process&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&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;2000&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;/span&gt;
&lt;span&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="https://gist.github.com/Nooshu/a6299b047d6f88a586de9b126216b49e"&gt;Gist here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id="other-technical-details-worth-mentioning"&gt;Other Technical details worth mentioning&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#other-technical-details-worth-mentioning"&gt;&lt;span&gt;Jump to section titled: Other Technical details worth mentioning&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Here are 4 other small technical details worth explaining as part of the implementation:&lt;/p&gt;&lt;h3 id="why-encodebody-manual-is-required"&gt;Why encodeBody: 'manual' is required&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#why-encodebody-manual-is-required"&gt;&lt;span&gt;Jump to section titled: Why encodeBody: 'manual' is required&lt;/span&gt;&lt;/a&gt;&lt;p&gt;The content we send back to the user'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'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'&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's browser.&lt;/p&gt;&lt;h3 id="why-vary-accept-encoding-and-cache-control-no-transform-matter"&gt;Why Vary: Accept-Encoding and Cache-Control: no-transform matter&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#why-vary-accept-encoding-and-cache-control-no-transform-matter"&gt;&lt;span&gt;Jump to section titled: Why Vary: Accept-Encoding and Cache-Control: no-transform matter&lt;/span&gt;&lt;/a&gt;&lt;p&gt;These 2 response headers are critical for making the pre-compression work. I'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'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'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="incremental-build-optimisation-runtime-check-to-skip-unchanged-files"&gt;Incremental build optimisation (runtime check to skip unchanged files)&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#incremental-build-optimisation-runtime-check-to-skip-unchanged-files"&gt;&lt;span&gt;Jump to section titled: Incremental build optimisation (runtime check to skip unchanged files)&lt;/span&gt;&lt;/a&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="why-we-need-a-cloudflare-function-instead-of-just-the-headers-file-for-html-brotli"&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;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#why-we-need-a-cloudflare-function-instead-of-just-the-headers-file-for-html-brotli"&gt;&lt;span&gt;Jump to section titled: Why we need a Cloudflare Function instead of just the _headers file for HTML Brotli?&lt;/span&gt;&lt;/a&gt;&lt;h4 id="1-headers-can-only-change-headers-not-the-file-itself"&gt;1. &lt;code&gt;_headers&lt;/code&gt; can only change headers, not the file itself&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#1-headers-can-only-change-headers-not-the-file-itself"&gt;&lt;span&gt;Jump to section titled: 1. _headers can only change headers, not the file itself&lt;/span&gt;&lt;/a&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'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="2-setting-the-header-alone-is-not-enough"&gt;2. Setting the header alone is not enough&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#2-setting-the-header-alone-is-not-enough"&gt;&lt;span&gt;Jump to section titled: 2. Setting the header alone is not enough&lt;/span&gt;&lt;/a&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't compressed, it would simply fail and the page would break.&lt;/p&gt;&lt;h2 id="results"&gt;Results&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#results"&gt;&lt;span&gt;Jump to section titled: Results&lt;/span&gt;&lt;/a&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;&#128202; 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's examine the results from the DevTools Network panel below:&lt;/p&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#devtools-before"&gt;&lt;span&gt;Jump to section titled: DevTools Before&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;source type="image/webp"&gt;&lt;img alt="The Firefox DevTools panel before implementation, no Brotli compression in the Content-Encoding response header Network panel column" src="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/NnpcUW5tX8-300.png"&gt;&lt;/source&gt;&lt;/p&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#devtools-after"&gt;&lt;span&gt;Jump to section titled: DevTools After&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;source type="image/webp"&gt;&lt;img alt="The Firefox DevTools panel before implementation, Brotli compression can be seen in the Content-Encoding response header Network panel column" src="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/16NWb1-HYF-300.png"&gt;&lt;/source&gt;&lt;/p&gt;&lt;h3 id="devtools-after-page-reload"&gt;DevTools After Page Reload&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#devtools-after-page-reload"&gt;&lt;span&gt;Jump to section titled: DevTools After Page Reload&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;source type="image/webp"&gt;&lt;img alt="The Firefox DevTools panel on page reload, no Brotli compression in the Content-Encoding response header Network panel column can be seen" src="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/VCyv4pYajj-300.png"&gt;&lt;/source&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="summary"&gt;Summary&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#summary"&gt;&lt;span&gt;Jump to section titled: Summary&lt;/span&gt;&lt;/a&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="https://en.wikipedia.org/wiki/Edge_computing"&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="https://nooshu.com/contact/"&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;/ul&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Matt Hobbs</name>
        </author>
        <media:content medium="image" url="https://nooshu.com/og-images/blog-2026-02-21-precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers.png"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://nooshu.com/feed/feed-rss.xml</id>
            <title type="html">Nooshu</title>
            <link href="https://nooshu.com" rel="alternate" type="text/html"/>
            <updated>2026-02-25T10:50:47Z</updated>
        </source>
    </entry>
</feed>