<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Ben White — Blog</title>
    <subtitle>Send me your address so I can visit you and explain my passions.</subtitle>
    <link href="https://benwhite.com.au/rss/blog.xml" rel="self"/>
    <link href="https://benwhite.com.au/"/>
    <updated>2026-06-23T00:00:00Z</updated>
    <id>https://benwhite.com.au/</id>
    <author>
        <name>Ben White</name>
    </author>
    <entry>
        <title>Getting through the backlog</title>
        <link href="https://benwhite.com.au/blog/the-backlog/"/>
        <updated>2026-06-23T00:00:00Z</updated>
        <id>https://benwhite.com.au/blog/the-backlog/</id>
        <content type="html">&lt;html&gt;
  &lt;head&gt;&lt;/head&gt;
  &lt;body&gt;
    &lt;p&gt;I have a one-game policy, mainly because I&#39;m old and have a family and bills and taxes. So I basically just play The Finals every night. You&#39;d think I&#39;d be pretty good at The Finals by now!&lt;/p&gt;
    &lt;p&gt;You&#39;d think.&lt;/p&gt;
    &lt;p&gt;My one-game policy is like any good policy, it&#39;s a rough guideline. Here are the games I&#39;ve been breaking my rule for lately.&lt;/p&gt;
    &lt;h3&gt;Arc Raiders&lt;/h3&gt;
    &lt;p&gt;&lt;a href=&quot;https://store.steampowered.com/app/1808500/ARC_Raiders/&quot; target=&quot;_blank&quot;&gt;Arc Raiders&lt;/a&gt; was my overtime for a while. Everyone goes to sleep, it&#39;s Arc Raiders time, baby. Playing this game is to accept its stratospheric highs and the deepest, darkest lows.&lt;/p&gt;
    &lt;p&gt;I like to &lt;em&gt;think&lt;/em&gt; I&#39;m &lt;a href=&quot;https://benwhite.com.au/blog/first-principles/&quot; target=&quot;_blank&quot;&gt;pragmatic and principled&lt;/a&gt;, but Arc Raiders made me question that self truth. I see myself as a friendly raider. Sure, I&#39;m looting for me, but also for you. We&#39;re in this together buddy! But after getting shot in the back a few times, suddenly I&#39;m heading topside for blood. Huh, turns out I&#39;m not as principled as I thought.&lt;/p&gt;
    &lt;p&gt;The game feels different these days, what with all the racists, sociopaths and cheaters. Time for another game to fill the void.&lt;/p&gt;
    &lt;hr /&gt;
    &lt;h3&gt;Clair Obscur: Expedition 33&lt;/h3&gt;
    &lt;p&gt;
      I&#39;m pretty sure &lt;a href=&quot;https://store.steampowered.com/app/1903340/Clair_Obscur_Expedition_33/&quot; target=&quot;_blank&quot;&gt;Clair Obscur: Expedition 33&lt;/a&gt; won every &quot;Game of the year&quot; award when it came out &lt;sup&gt;[&lt;em&gt;citation needed&lt;/em&gt;]&lt;/sup&gt;. All I knew going in was: &quot;cool game by small French team&quot;.
    &lt;/p&gt;
    &lt;p&gt;Within the first few minutes I was like: &quot;Why yes, this is absolutely the most French game I&#39;ve ever played&quot; and switched to French voice acting, for the complete experience.&lt;/p&gt;
    &lt;p&gt;When the first combat tutorial popped up, I closed my eyes and sighed. Turn based? Thanks. I hate it. But I persevered, and boy did that pay off.&lt;/p&gt;
    &lt;p&gt;Clair Obscur: Expedition 33 is a complex, layered, superbly written and beautifully realised story about grief.&lt;/p&gt;
    &lt;p&gt;The voice acting and writing is just [chef&#39;s kiss]. The combat ended up being... really fun!?? After about 40 hours I had finished the main story and the relationship missions. Even though it felt like the game had a lot more to give, I was ready to move on.&lt;/p&gt;
    &lt;hr /&gt;
    &lt;h3&gt;Mixtape&lt;/h3&gt;
    &lt;p&gt;I don&#39;t really pay close attention to the discourse, but from what I could tell, &lt;a href=&quot;https://store.steampowered.com/app/2582320/Mixtape/&quot; target=&quot;_blank&quot;&gt;Mixtape&lt;/a&gt; released to widespread critical acclaim, followed by a somewhat predictable backlash from the &quot;Gamers&quot;.&lt;/p&gt;
    &lt;p&gt;Knowledge of backlash was nestled in my mind for my play through. I was sure I&#39;d be able to spot the moment that the &quot;Gamers&quot; didn&#39;t like. As the credits rolled, I was like, what the hell? Why are people mad about this game? I went to sleep, and the next day I listened to the Mixtape soundtrack (it&#39;s great). Turns out I&#39;m not that interested in what the &quot;Gamers&quot; are angry about. I don&#39;t have space in my life for that nonsense.&lt;/p&gt;
    &lt;p&gt;Was the game life changing? Not really! The main character is annoying and pretentious, like, uh, I was? When I was a teenager? The friend group dynamic looks pretty straightforward on the surface, but body language shows it isn&#39;t. And I really like how sometimes, the game isn&#39;t interested in going into any of that (sorry Slater).&lt;/p&gt;
    &lt;p&gt;At some point, you end up drunk in a Blockbuster. The mission: pick out a few movies and get back to the car. At this point I was still playing the game like how I&#39;ve been trained. Get task, do task, get reward. But as I&#39;m stumbling through the store I bump into a shelf and tapes go flying. Finally! It only took half the bloody game, but finally, I got it. Yes! You &lt;em&gt;can&lt;/em&gt; carefully walk in, pick the videos and move onto the next scene, but you&#39;d be missing the entire point. You are a drunk teenager in a video store. The point is chaos! Teenage angst and confusion, rage and joy: The Video Game. After this, I started to really appreciate Mixtape a lot. I loved my handful of hours with it.&lt;/p&gt;
    &lt;hr /&gt;
    &lt;p&gt;So my backlog is a little smaller, my heart a little more full.&lt;/p&gt;
    &lt;p&gt;On to Pragmata!&lt;/p&gt;
  &lt;/body&gt;
&lt;/html&gt;</content>
    </entry>
    <entry>
        <title>First principles</title>
        <link href="https://benwhite.com.au/blog/first-principles/"/>
        <updated>2025-12-03T00:00:00Z</updated>
        <id>https://benwhite.com.au/blog/first-principles/</id>
        <content type="html">&lt;html&gt;
  &lt;head&gt;&lt;/head&gt;
  &lt;body&gt;
    &lt;p&gt;In a meeting room in 2016, my ears perked up. A much smarter person than me had taken a moment to reflect and said, “Let’s bring this back to first principles”. I nodded like I knew what that meant. I mean, I had a general idea... but I didn&#39;t realise the magical power that incantation holds.&lt;/p&gt;
    &lt;p&gt;Many times since, I’ve seen those words duck and weave the strongest egos, opinions, and personalities. I could enter rooms with wildly complex problems, each with endless, conflicting potential solutions, and somehow leave with everyone pointed in the same direction, nodding like Robert Redford.&lt;/p&gt;
    &lt;picture
      &gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://benwhite.com.au/img/FZq3VmvpSE-390.webp 390w&quot; /&gt;
      &lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://benwhite.com.au/img/FZq3VmvpSE-390.gif&quot; alt=&quot;Animated image of Robert Redford, the camera moves towards his face slowly, and he nods. The image fills you with satisfaction.&quot; width=&quot;390&quot; height=&quot;163&quot;
    /&gt;&lt;/picture&gt;
    &lt;p&gt;Recently, I’ve been trying to get my head around what part of our system should do the heavy lifting for any given new feature. As we added new capabilities to our stack, paths that were previously clear cut became murky. I wanted a framework that would help me burn off the fog and help me make choices that were simple to reason and explain. Maybe first principles would help?&lt;/p&gt;
    &lt;hr /&gt;
    &lt;p&gt;Capital Brief is built with &lt;a href=&quot;https://www.11ty.dev/&quot; target=&quot;_blank&quot;&gt;11ty&lt;/a&gt; (a static site generator), has access to &lt;a href=&quot;https://www.cloudflare.com/en-gb/developer-platform/products/workers/&quot; target=&quot;_blank&quot;&gt;compute at the edge&lt;/a&gt;, and sprinkles of &lt;a href=&quot;https://alpinejs.dev/&quot; target=&quot;_blank&quot;&gt;AlpineJS&lt;/a&gt; and &lt;a href=&quot;http://htmx.org/&quot; target=&quot;_blank&quot;&gt;HTMX&lt;/a&gt; in the client. So when it comes to building a new widget, we have plenty of options.&lt;/p&gt;
    &lt;p&gt;Let’s come up with some first principles:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;Do hard work early.&lt;/li&gt;
      &lt;li&gt;Only compute when absolutely necessary.&lt;/li&gt;
      &lt;li&gt;Keep the client thin.&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p&gt;Now, let those first principles guide the thinking for some potential website changes.&lt;/p&gt;
    &lt;blockquote&gt;
      &lt;p&gt;We need to add a new menu item and related paginated index of stories.&lt;/p&gt;
    &lt;/blockquote&gt;
    &lt;p&gt;Let’s generate the menu and index at build time and ship them as plain HTML (hard work early). This adds some compute to the build but avoids adding runtime logic (keep the client thin).&lt;/p&gt;
    &lt;hr /&gt;
    &lt;blockquote&gt;
      &lt;p&gt;We need to check the user’s JWT token and serve different pages to different users.&lt;/p&gt;
    &lt;/blockquote&gt;
    &lt;p&gt;Let’s validate the JWT and determine the user’s access level at the edge (keep the client thin), then fetch and serve a pre-built page variant (hard work early).&lt;/p&gt;
    &lt;hr /&gt;
    &lt;blockquote&gt;
      &lt;p&gt;We need to add cachebusting to our CSS and JS file paths.&lt;/p&gt;
    &lt;/blockquote&gt;
    &lt;p&gt;Let’s generate hashed asset filenames at build (hard work early), store a simple mapping, and let a lightweight edge worker rewrite CSS/JS URLs on the fly so the HTML doesn’t need to be rebuilt for every change (reduce compute).&lt;/p&gt;
    &lt;hr /&gt;
    &lt;blockquote&gt;
      &lt;p&gt;We need our ad placements to update immediately after changes in the CMS.&lt;/p&gt;
    &lt;/blockquote&gt;
    &lt;p&gt;Let’s serve static pages with empty ad slots (reduce compute) and use a tiny API/HTMX endpoint that reads the latest pre-built ad config (hard work early) to fill those slots.&lt;/p&gt;
    &lt;hr /&gt;
    &lt;p&gt;Through the lens of first principles, the endless possible solutions for each problem narrow immediately. There’s less ambiguity. We find our path faster, and what we build will more likely align with patterns we’ve already made.&lt;/p&gt;
    &lt;p&gt;Neat!&lt;/p&gt;
  &lt;/body&gt;
&lt;/html&gt;</content>
    </entry>
    <entry>
        <title>Migrating an 11ty site from Cloudflare Pages to Workers</title>
        <link href="https://benwhite.com.au/blog/pages-to-workers/"/>
        <updated>2025-08-04T00:00:00Z</updated>
        <id>https://benwhite.com.au/blog/pages-to-workers/</id>
        <content type="html">&lt;html&gt;
  &lt;head&gt;&lt;/head&gt;
  &lt;body&gt;
    &lt;p&gt;If you&#39;re reading this, I&#39;m guessing you&#39;ve got an 11ty site live on Cloudflare Pages somewhere, and you&#39;ve probably come across one of the &lt;em&gt;many&lt;/em&gt; Cloudflare messages and prompts asking you nicely to move off Pages and over to Workers. It&#39;s a bummer, because Cloudflare Pages is a really great product!&lt;/p&gt;
    &lt;p&gt;If you&#39;ve used GitHub Pages or Netlify, Cloudflare Pages is easy to pick up. But diving into the &lt;a href=&quot;https://developers.cloudflare.com/workers/&quot; target=&quot;_blank&quot;&gt;Worker docs&lt;/a&gt; (&lt;a href=&quot;https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/&quot; target=&quot;_blank&quot;&gt;and even the Migration docs&lt;/a&gt;), they can seem pretty complicated, because, well... they are. For now, let&#39;s try and skip past all the complex stuff and just get a Worker to serve our 11ty build.&lt;/p&gt;
    &lt;p&gt;I&#39;m gonna start with some assumptions:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;You have a working 11ty site, which is currently running on Cloudflare Pages&lt;/li&gt;
      &lt;li&gt;You can build your 11ty site on your own machine&lt;/li&gt;
      &lt;li&gt;We&#39;re skipping multiple environments, secrets and environment variables for now, and just spinning up on local and prod (you can have as many environments as you want, but let&#39;s keep it simple for now)&lt;/li&gt;
      &lt;li&gt;We&#39;re going to set things up with commands in a terminal (this is not strictly required, you can set up Workers via the UI, but do note that changes in the UI will be lost the next time you deploy via terminal and vice versa -- best to choose your preferred method of working and stick with that)&lt;/li&gt;
      &lt;li&gt;We&#39;re going to focus on just the site HTML for now, Cloudflare Pages functions or middleware can come later&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p&gt;Let&#39;s get into it!&lt;/p&gt;
    &lt;h3&gt;1. Install &lt;code&gt;wrangler&lt;/code&gt; and login&lt;/h3&gt;
    &lt;p&gt;
      &lt;code&gt;wrangler&lt;/code&gt; is the tool for managing your Worker (both for local development, and deploying to production). &lt;a href=&quot;https://developers.cloudflare.com/workers/wrangler/install-and-update/&quot; target=&quot;_blank&quot;&gt;The docs recommend installing &lt;code&gt;wrangler&lt;/code&gt; as a project dependency&lt;/a&gt; via &lt;code&gt;npm i -D wrangler@latest&lt;/code&gt;. That&#39;s generally good advice, but if you prefer managing things via &lt;code&gt;brew&lt;/code&gt;, or want to install it globally, you can absolutely just do that.
    &lt;/p&gt;
    &lt;p&gt;Once installed, run &lt;code&gt;npx wrangler login&lt;/code&gt;, which will open a browser window asking you to sign in to Cloudflare and authorise &lt;code&gt;wrangler&lt;/code&gt;.&lt;/p&gt;
    &lt;h3&gt;2. Configure your Worker&lt;/h3&gt;
    &lt;p&gt;In your project root (probably where your 11ty config lives), add a new &lt;code&gt;wrangler.jsonc&lt;/code&gt; file (&lt;code&gt;wrangler.toml&lt;/code&gt; is also valid, but JSONC seems like the recommended path these days).&lt;/p&gt;
    &lt;p&gt;You might already have this file if you deployed your Pages project via code. If so, make sure to remove &lt;code&gt;pages_build_output_dir&lt;/code&gt;.&lt;/p&gt;
    &lt;p&gt;Below is an example for my site, you can fill in the blanks.&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;website-benwhite&quot;,
  &quot;compatibility_date&quot;: &quot;2025-05-05&quot;,
  &quot;assets&quot;: {
    &quot;directory&quot;: &quot;_site&quot;,
    &quot;not_found_handling&quot;: &quot;404-page&quot;,
  },
  &quot;routes&quot;: [{ &quot;pattern&quot;: &quot;benwhite.com.au&quot;, &quot;custom_domain&quot;: true }],
}
&lt;/code&gt;&lt;/pre&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code&gt;name&lt;/code&gt;: This will be the name of the Worker in the Cloudflare UI, which is only public if you choose to use the &lt;code&gt;{worker-name}.workers.dev&lt;/code&gt; URL for anything&lt;/li&gt;
      &lt;li&gt;&lt;code&gt;assets.directory&lt;/code&gt;: You only need to update this if you&#39;ve set 11ty up to build somewhere else, &lt;code&gt;_site&lt;/code&gt; is the default 11ty build path&lt;/li&gt;
      &lt;li&gt;&lt;code&gt;assets.not_found_handling&lt;/code&gt;: This will direct users to the &lt;code&gt;404.html&lt;/code&gt; file in your &lt;code&gt;_site&lt;/code&gt; directory. If you don&#39;t have a 404 file in your project, or would like to handle 404s differently, &lt;a href=&quot;https://developers.cloudflare.com/workers/static-assets/#routing-behavior&quot; target=&quot;_blank&quot;&gt;have a browse through the routing behaviour docs&lt;/a&gt;. There is also &lt;a href=&quot;https://developers.cloudflare.com/workers/static-assets/routing/static-site-generation/#reference&quot; target=&quot;_blank&quot;&gt;a neat diagram on how the worker makes routing choices here&lt;/a&gt;.&lt;/li&gt;
      &lt;li&gt;&lt;code&gt;routes[0].pattern&lt;/code&gt;: Make sure you add the &lt;code&gt;www.&lt;/code&gt; to your own domain, if that&#39;s how your site is currently set up!&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p&gt;The assets config is basically the most important bit, and tells the Worker where to find all the files for your site build. The &lt;a href=&quot;https://developers.cloudflare.com/workers/static-assets/&quot; target=&quot;_blank&quot;&gt;Static Assets docs have a bunch more detail&lt;/a&gt;.&lt;/p&gt;
    &lt;p&gt;Defining the route in code is just a neat way to reduce the clicks needed in connecting your DNS to the Worker. But as mentioned above, if you want to handle the routes via the UI instead, you can just remove that line.&lt;/p&gt;
    &lt;p&gt;By not defining a &lt;code&gt;main&lt;/code&gt; script file AND setting up &lt;code&gt;assets&lt;/code&gt;, we&#39;re letting Cloudflare know we want to create an &quot;assets-only Worker&quot;. If you want to change this later to move over your Cloudflare Pages Functions, that&#39;s totally fine. All of these choices can be changed later without fuss.&lt;/p&gt;
    &lt;h3&gt;3. Update &lt;code&gt;.gitignore&lt;/code&gt; and &lt;code&gt;package.json&lt;/code&gt;.&lt;/h3&gt;
    &lt;p&gt;Add the &lt;code&gt;.wrangler/&lt;/code&gt; directory to &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;
    &lt;p&gt;Now, it&#39;s time to connect some dots with &lt;code&gt;package.json&lt;/code&gt; scripts. Here&#39;s how I&#39;ve done it:&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
  &quot;11ty:build&quot;: &quot;npx @11ty/eleventy&quot;,
  &quot;11ty:watch&quot;: &quot;npx @11ty/eleventy --watch --quiet --ignore-initial&quot;,
  &quot;11ty:benchmark&quot;: &quot;DEBUG=Eleventy:Benchmark* npm run build&quot;,
  &quot;build&quot;: &quot;11ty:build&quot;,
  &quot;util:rimraf&quot;: &quot;npx rimraf _site&quot;,
  &quot;util:killport&quot;: &quot;npx kill-port --port 8787&quot;,
  &quot;start&quot;: &quot;npm-run-all util:rimraf util:killport 11ty:build --parallel 11ty:watch wrangler:dev&quot;,
  &quot;wrangler:dev&quot;: &quot;npx wrangler dev --live-reload&quot;,
  &quot;wrangler:deploy&quot;: &quot;npx wrangler deploy&quot;
},
&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;No two &lt;code&gt;package.json&lt;/code&gt; files are alike, so I&#39;ll just step through the important stuff:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code&gt;11ty:build&lt;/code&gt;: Just do the build, nice and simple&lt;/li&gt;
      &lt;li&gt;&lt;code&gt;11ty:watch&lt;/code&gt;: Run build if something changes, used for local dev only. We&#39;re using &lt;code&gt;--ignore-initial&lt;/code&gt; here because we want to run a regular 11ty build beforehand&lt;/li&gt;
      &lt;li&gt;&lt;code&gt;start&lt;/code&gt;: &lt;code&gt;wrangler&lt;/code&gt; gets a bit confused if &lt;code&gt;_site&lt;/code&gt; isn&#39;t ready to go. That&#39;s why we run the regular build before running the watch build&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p&gt;How you handle this is very much up to how you handle your own build, but they key takeaway is that your &lt;code&gt;11ty --watch&lt;/code&gt; process should run in parallel with &lt;code&gt;wrangler dev&lt;/code&gt;. This will allow live reload to work.&lt;/p&gt;
    &lt;h3&gt;4. Test and deploy&lt;/h3&gt;
    &lt;p&gt;Run &lt;code&gt;npm run start&lt;/code&gt; (or however you kick off your local stuff), and see if everything works as expected. You&#39;ll note &lt;code&gt;wrangler&lt;/code&gt; spins up your Worker on &lt;code&gt;8787&lt;/code&gt;. &lt;a href=&quot;https://developers.cloudflare.com/workers/wrangler/configuration/#local-development-settings&quot; target=&quot;_blank&quot;&gt;You can change that (and many other settings) if you want&lt;/a&gt;.&lt;/p&gt;
    &lt;p&gt;If everything is all okay, run the deploy script &lt;code&gt;npm run wrangler:deploy&lt;/code&gt; (or just run it directly with &lt;code&gt;npx wrangler deploy&lt;/code&gt;).&lt;/p&gt;
    &lt;p&gt;NOTE: If you set up your domain as a route in the &lt;code&gt;wrangler.jsonc&lt;/code&gt; but Cloudflare DNS already has a record assigned for that domain/subdomain, you will likely get an error here. You&#39;ll need to manually delete it via the Cloudflare DNS web UI before you&#39;re allowed to deploy. Keep a screenshot of those settings before removing, just in case you want to roll back.&lt;/p&gt;
    &lt;h3&gt;5. Optional: Connect the Worker to your Git repo&lt;/h3&gt;
    &lt;p&gt;This is optional. If you&#39;re fine running the build and deploy commands locally and manually, you don&#39;t need this. Enabling this connection will ensure any merge to &lt;code&gt;main&lt;/code&gt; will kick of a new build and deploy in Cloudflare instead. I couldn&#39;t find a way to configure this in &lt;code&gt;wrangler.jsonc&lt;/code&gt; yet, but &lt;a href=&quot;https://developers.cloudflare.com/workers/ci-cd/builds/&quot; target=&quot;_blank&quot;&gt;the docs recommend creating this links via the UI in any case&lt;/a&gt;.&lt;/p&gt;
    &lt;p&gt;And you&#39;re done!&lt;/p&gt;
    &lt;p&gt;
      I&#39;ll dive into moving functions over soon. Until then, I&#39;ve shared some &lt;a href=&quot;https://benwhite.com.au/snippets/pages-to-workers-config/&quot; target=&quot;_blank&quot;&gt;example &lt;code&gt;wrangler&lt;/code&gt; config snippets that should hopefully give you a head start&lt;/a&gt;.
    &lt;/p&gt;
    &lt;p&gt;If you&#39;re getting stuck with your move, I&#39;m happy to chat and help where I can, just ping me on &lt;a href=&quot;https://infosec.exchange/deck/@d3v1an7&quot; target=&quot;_blank&quot;&gt;Mastodon&lt;/a&gt;.&lt;/p&gt;
    &lt;h3&gt;Thank yous&lt;/h3&gt;
    &lt;p&gt;
      Big thank you &lt;a href=&quot;https://ottawa.place/@cassey&quot; target=&quot;_blank&quot;&gt;Cassey Lottman&lt;/a&gt; for their &lt;a href=&quot;https://ottawa.place/@cassey/114994652655776365&quot; target=&quot;_blank&quot;&gt;helpful feedback on &lt;code&gt;not_found_handling&lt;/code&gt;&lt;/a
      &gt;!
    &lt;/p&gt;
  &lt;/body&gt;
&lt;/html&gt;</content>
    </entry>
    <entry>
        <title>WebC at scale</title>
        <link href="https://benwhite.com.au/blog/webc-at-scale/"/>
        <updated>2024-08-07T00:00:00Z</updated>
        <id>https://benwhite.com.au/blog/webc-at-scale/</id>
        <content type="html">&lt;html&gt;
  &lt;head&gt;&lt;/head&gt;
  &lt;body&gt;
    &lt;p&gt;When setting out to build &lt;em&gt;Capital Brief&lt;/em&gt;, 11ty (v2 at the time) supported &lt;a href=&quot;https://www.netlify.com/blog/2021/04/14/faster-builds-for-large-sites-on-netlify-with-on-demand-builders-now-in-early-access/&quot; target=&quot;_blank&quot;&gt;on-demand builders in Netlify&lt;/a&gt; via the &lt;a href=&quot;https://www.11ty.dev/docs/plugins/serverless/&quot; target=&quot;_blank&quot;&gt;11ty serverless plugin&lt;/a&gt;. This allowed me to use a build strategy along the lines of:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;Build the most recent stories and first page of each index&lt;/li&gt;
      &lt;li&gt;Use on-demand builds for the rest&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p&gt;This gave us predicable build times and ensured the pages we expected humans to read were super fast.&lt;/p&gt;
    &lt;p&gt;Sounds good right? Unfortunately for me:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;The latest version of 11ty (v3 at time of writing) removes the Netlify based serverless plugin, and&lt;/li&gt;
      &lt;li&gt;The long tail of on-demand pages (which by design, are a bit slower on first access) has a negative impact in Google PageSpeed Insights&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p&gt;So the new build strategy is: &lt;strong&gt;build all the things&lt;/strong&gt;.&lt;/p&gt;
    &lt;p&gt;All fun and games in theory, but the second the rubber hit the road, the build failed with:&lt;/p&gt;
    &lt;p&gt;&lt;code&gt;FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory&lt;/code&gt;&lt;/p&gt;
    &lt;p&gt;I gave the process 8GB of RAM: &lt;code&gt;npx --node-options=&#39;--max-old-space-size=8192&#39; @11ty/eleventy --serve --quiet&lt;/code&gt;... and got the same error.&lt;/p&gt;
    &lt;p&gt;Uh oh.&lt;/p&gt;
    &lt;p&gt;So I started breaking things up into smaller chunks to see what part of the build was the most expensive. Here&#39;s what I found:&lt;/p&gt;
    &lt;h3&gt;Use @raw instead of @html&lt;/h3&gt;
    &lt;p&gt;Since the good old days, I&#39;ve used a layout pattern like:&lt;/p&gt;
    &lt;p&gt;&lt;strong&gt;base.webc&lt;/strong&gt;&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&gt;
&amp;lt;html
  &amp;lt;head&gt;&amp;lt;/head&gt;
  &amp;lt;body&gt;
    &amp;lt;template webc:nokeep @html=&quot;content&quot;&gt;&amp;lt;/template&gt;
  &amp;lt;/body&gt;
&amp;lt;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;&lt;strong&gt;page.webc&lt;/strong&gt;&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;---
layout: base.webc
---

&amp;lt;custom-header&gt;&amp;lt;/custom-header&gt;
&amp;lt;main @html=&quot;content&quot;&gt;&amp;lt;/main&gt;
&amp;lt;custom-footer&gt;&amp;lt;/custom-footer&gt;
&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;I&#39;m pretty sure it&#39;s a standard pattern, and from my first read of the WebC docs, the usage of &lt;code&gt;@html&lt;/code&gt; seems right?&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;- Content returned from the @html prop will be processed as WebC.
- Using [@raw] will prevent processing the result as WebC.
&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;Turns out that &lt;em&gt;in the context of a layout&lt;/em&gt;, using &lt;code&gt;@html&lt;/code&gt; for &lt;code&gt;content&lt;/code&gt; is not necessary, and just super expensive.&lt;/p&gt;
    &lt;p&gt;Out of everything I discovered, this is the biggest, easiest, and most practical win. I haven&#39;t dug into the internals to work out why, but this change is what allowed the build to complete without running out of memory.&lt;/p&gt;
    &lt;h3&gt;Avoid nesting WebC components, where practical&lt;/h3&gt;
    &lt;p&gt;This change had a smaller impact, but I noticed improvements when reducing component and layout nesting, particularly where the data being passed through was chunky. As a follow up example from above, instead of splitting the layouts into &lt;code&gt;base&lt;/code&gt; + &lt;code&gt;page&lt;/code&gt;, I now use use just &lt;code&gt;page&lt;/code&gt;.&lt;/p&gt;
    &lt;p&gt;&lt;strong&gt;page.webc&lt;/strong&gt;&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&gt;
&amp;lt;html
  &amp;lt;head&gt;&amp;lt;/head&gt;
  &amp;lt;body&gt;
    &amp;lt;custom-header&gt;&amp;lt;/custom-header&gt;
    &amp;lt;main @raw=&quot;content&quot;&gt;&amp;lt;/main&gt;
    &amp;lt;custom-footer&gt;&amp;lt;/custom-footer&gt;
  &amp;lt;/body&gt;
&amp;lt;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
    &lt;h3&gt;Get expensive data into required shape&lt;/h3&gt;
    &lt;p&gt;If you&#39;re in a position where every millisecond counts, you can shave a few off by getting complex or looped data ready as HTML in your data first, then use &lt;code&gt;&amp;lt;div @raw=&quot;related.html&quot;&gt;&amp;lt;/div&gt;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;div webc:for=&quot;related.items&quot;&gt;...&amp;lt;/div&gt;&lt;/code&gt;.&lt;/p&gt;
    &lt;p&gt;I actually rolled this change back in the end, as the many costs of the change outweighed the benefit. I much, much prefer to handle the HTML in the template rather than in data.&lt;/p&gt;
    &lt;hr /&gt;
    &lt;h3&gt;TLDR;&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Use of some WebC props get expensive as the number of files to build grows.&lt;/li&gt;
      &lt;li&gt;Using &lt;code&gt;@raw&lt;/code&gt; instead of &lt;code&gt;@html&lt;/code&gt; will provide the biggest performance boost, particularly if used in layout files.&lt;/li&gt;
      &lt;li&gt;Nesting of WebC components can slow things down, especially if you&#39;re passing large objects or content as props.&lt;/li&gt;
      &lt;li&gt;You can trim a few milliseconds off by pre-processing complex HTML, but doing this might not be worth the many tradeoffs.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/body&gt;
&lt;/html&gt;</content>
    </entry>
    <entry>
        <title>Nested pagination with 11ty</title>
        <link href="https://benwhite.com.au/blog/nested-pagination/"/>
        <updated>2024-06-27T00:00:00Z</updated>
        <id>https://benwhite.com.au/blog/nested-pagination/</id>
        <content type="html">&lt;html&gt;
  &lt;head&gt;&lt;/head&gt;
  &lt;body&gt;
    &lt;p&gt;Let&#39;s kick off with a caveat: I don&#39;t rely on 11ty collections for building indexes and loops and stuff. I pull down the data I need from a CMS, get it into shape, then use the cleaned up data in WebC template files. This keeps almost all data related transformations in one place (the data file), and allows the templates to be super clean.&lt;/p&gt;
    &lt;p&gt;Everything that follows here is based on that usage of 11ty data, but I reckon you should be able to follow a similar pattern when building a new collection, if that&#39;s more your jam.&lt;/p&gt;
    &lt;p&gt;Let&#39;s say we have a &lt;code&gt;_data/all.js&lt;/code&gt; file that grabs all articles from a CMS, ending up with something like this:&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;const articles = [
  {
    title: &#39;Example article&#39;,
    authors: [{ slug: &#39;jake-dog&#39; }, { slug: &#39;susan-strong&#39; }],
  },
  {
    title: &#39;Another example&#39;,
    authors: [{ slug: &#39;finn-human&#39; }],
  },
  [etc...]
]
&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;Now, we want to create some author indexes (&lt;code&gt;/author/jake-dog/&lt;/code&gt;), that are also paginated (&lt;code&gt;/author/jake-dog/2/&lt;/code&gt;).&lt;/p&gt;
    &lt;p&gt;A data structure that I reckon makes sense here is an array of authors that each have an array of articles.&lt;/p&gt;
    &lt;p&gt;Unfortunately for us, 11ty pagination only handles a single data set: you can either loop through the authors, or the authors articles, not both. We &lt;em&gt;could&lt;/em&gt; solve this by just creating an individual template file for every author, but that doesn&#39;t sound like fun. Let&#39;s come up with something cleverer! Inspired by &lt;a href=&quot;https://www.codeflood.net/blog/2024/04/17/11ty-nested-pagination/&quot; target=&quot;_blank&quot;&gt;this post&lt;/a&gt; and &lt;a href=&quot;https://github.com/11ty/eleventy/issues/332&quot; target=&quot;_blank&quot;&gt;this thread&lt;/a&gt;, I ended up forming an array of authors that instead looked like this:&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;const authors = [
  {
    path: &#39;/author/jake-dog/&#39;,
    author: &#39;jake-dog&#39;,
    articles: [ [Object], [Object], [Object] ]
  },
  {
    path: &#39;/author/jake-dog/2/&#39;,
    author: &#39;jake-dog&#39;,
    articles: [ [Object], [Object], [Object] ]
  },
  {
    path: &#39;/author/susan-strong/&#39;,
    author: &#39;susan-strong&#39;,
    articles: [ [Object], [Object], [Object] ]
  },
  [etc...]
]
&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;Now we have a flat array, where each object represents an author index page to be built. Based on that, the WebC template code for our author index pages looks like this:&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;---js
{
  pagination: {
    data: &#39;all.authors&#39;,
    alias: &#39;item&#39;,
    size: 1,
  },
  permalink: ({ item }) =&gt; {
    return item.path;
  }
}
---

&amp;lt;h1 @text=&quot;item.author&quot;&gt;&amp;lt;/h1&gt;
&amp;lt;ul webc:for=&quot;article of item.articles&quot;&gt;
  &amp;lt;li @text=&quot;article.title&quot;&gt;&amp;lt;/li&gt;
&amp;lt;/ul&gt;
&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;Clean as!&lt;/p&gt;
    &lt;p&gt;
      &lt;small&gt;P.S. I&#39;m not a huge fan of the AI-fication of everything right now -- but I ain&#39;t gonna lie -- I use ChatGPT quite a bit for tasks like &lt;em&gt;&#39;pls make this data structure look more like this one over here&#39;&lt;/em&gt;.&lt;/small&gt;
    &lt;/p&gt;
    &lt;hr /&gt;
    &lt;h3&gt;TLDR;&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;As of today, 11ty only supports a single data set for pagination, and does not have a method to handle further pagination of any nested data sets.&lt;/li&gt;
      &lt;li&gt;You can work around this by creating a flat array, where each object contains enough info to build a single page.&lt;/li&gt;
      &lt;li&gt;Where you do this work is totally up to you. I like to do it in &lt;code&gt;_data&lt;/code&gt;, not in the template.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/body&gt;
&lt;/html&gt;</content>
    </entry>
    <entry>
        <title>Magic links are great, until they&#39;re not</title>
        <link href="https://benwhite.com.au/blog/magic-links/"/>
        <updated>2024-06-19T00:00:00Z</updated>
        <id>https://benwhite.com.au/blog/magic-links/</id>
        <content type="html">&lt;html&gt;
  &lt;head&gt;&lt;/head&gt;
  &lt;body&gt;
    &lt;p&gt;It was a short discussion when determining the auth method for &lt;a href=&quot;https://www.capitalbrief.com/&quot; target=&quot;_blank&quot;&gt;Capital Brief&lt;/a&gt;. Some flavour of passwordless was a must, and when thinking back to passwordless experiences that brought me joy, Slack came to mind.&lt;/p&gt;
    &lt;p&gt;I remembered opening Slack on my phone for the first time. I entered my email address and hit sign in, expecting a password prompt to follow. But instead, bing! An email with a sign in link. I opened the link and... whoa, I am signed in to Slack. Just like magic.&lt;/p&gt;
    &lt;p&gt;That flow felt great to me! I wanted to replicate that.&lt;/p&gt;
    &lt;p&gt;I experimented with a few auth providers, chose one, built the thing and launched it.&lt;/p&gt;
    &lt;p&gt;Things seemed to be going well, but then:&lt;/p&gt;
    &lt;blockquote&gt;
      &lt;p&gt;“When I try to sign in, the site says the link has expired.”&lt;/p&gt;
    &lt;/blockquote&gt;
    &lt;p&gt;Uh oh, that doesn&#39;t seem like a nice experience. Turns out we have some readers that work in, let&#39;s say, fairly corporate environments. Their security teams want to protect their people from phishing. That&#39;s good!&lt;/p&gt;
    &lt;p&gt;Unfortunately, checking emails for phishing requires an automated process to open links, and... oh no, the link checker has the sign in token now, not the person trying to sign in. That&#39;s bad.&lt;/p&gt;
    &lt;p&gt;We had a chat with our US based auth provider and they said they already have checks on their end to stop bots from expiring the link. Perhaps we use different security tooling configs here in Australia? With no quick fix in sight, I switched those readers over to one-time password (OTP) instead. Besides the initial friction this seemed to work really well, so we didn&#39;t worry to much about magic links, until:&lt;/p&gt;
    &lt;blockquote&gt;
      &lt;p&gt;“The site keeps signing me out.”&lt;/p&gt;
    &lt;/blockquote&gt;
    &lt;p&gt;This one took a while to figure out. So let&#39;s say a reader is using the LinkedIn app and clicks through to a Capital Brief story. They see the paywall, get a magic link, sign in, read the story, close the app and do it all again the next day. But wait, now the site says they&#39;re not signed in anymore? What gives?&lt;/p&gt;
    &lt;p&gt;When a person opens a web link from a mobile app, the link typically doesn&#39;t open in the person&#39;s default browser. The link will instead open up in a browser embedded into the application (a &quot;webview&quot;) that does not share the state, sessions or cookies of the person&#39;s default browser. The person wasn&#39;t getting signed out – they were never being signed in to the webview in the first place. Instead, they&#39;re signed in to whatever browser opened when they clicked the sign in link from their email.&lt;/p&gt;
    &lt;p&gt;We directed people to the plain text link they could copy and paste into the desired browser (or webview), but this felt... extremely suboptimal. Then:&lt;/p&gt;
    &lt;blockquote&gt;
      &lt;p&gt;“We can only receive work emails in Teams, and links open in a sandboxed browser.”&lt;/p&gt;
    &lt;/blockquote&gt;
    &lt;p&gt;Lmao, okay, we can&#39;t fix this at all.&lt;/p&gt;
    &lt;p&gt;Earlier this year we removed magic links and switched all users to OTP, to the joy and delight of our support team (hello Dave).&lt;/p&gt;
    &lt;p&gt;So if you&#39;re building an application, maybe you&#39;ll be fine? I think Slack still use &#39;em? But if you&#39;re building a website, it&#39;d recommend sticking with OTP.&lt;/p&gt;
    &lt;hr /&gt;
    &lt;h3&gt;TLDR;&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Link checkers/email security tools can accidently expire magic links.&lt;/li&gt;
      &lt;li&gt;The magic link flow is super janky when trying to sign in to a website that was loaded in a webview.&lt;/li&gt;
      &lt;li&gt;Some people straight up can&#39;t make use of magic links to sign in to websites because of security policies.&lt;/li&gt;
      &lt;li&gt;Magic links aren&#39;t ready for wide adoption as a flow for signing into websites, so we switched to OTP instead.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/body&gt;
&lt;/html&gt;</content>
    </entry>
</feed>