<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss/styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ben Silverman</title><description>My personal website!</description><link>https://bensilverman.co.uk/</link><item><title>Who will I be in 75 days?</title><link>https://bensilverman.co.uk/posts/75soft/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/75soft/</guid><pubDate>Wed, 07 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you&amp;#39;re reading this after the 20th March, 2026, I&amp;#39;ll be a completely different person...I think? Or maybe I&amp;#39;ll be me, but better? Or the same person in almost every day but very slightly different in one extremely important but equally intangible factor? Who knows!&lt;/p&gt;
&lt;p&gt;To elaborate, my girlfriend and I (and a friend of ours) will be attempting &lt;a href=&quot;https://health.clevelandclinic.org/75-soft-challenge&quot;&gt;75 Soft&lt;/a&gt; this year, and I&amp;#39;m interested and excited to see what we&amp;#39;re like once we&amp;#39;re out the other end. For those who don&amp;#39;t know, 75 Soft is a &amp;quot;self-improvement challenge&amp;quot; where, for a period of 75 days (what a coincidence), you do your best to follow this set of rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Eat well, and avoid alcohol outside of social situations.&lt;/li&gt;
&lt;li&gt;Exercise once for 45 minutes a day, with one day of &amp;quot;active recovery&amp;quot; each week.&lt;/li&gt;
&lt;li&gt;Drink 3 litres of water a day.&lt;/li&gt;
&lt;li&gt;Read 10 pages of any book, every day.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Unlike &lt;a href=&quot;https://www.bbc.co.uk/news/articles/c5ydj07r0gro&quot;&gt;75 &lt;em&gt;Hard&lt;/em&gt;&lt;/a&gt;—the challenge &lt;em&gt;this&lt;/em&gt; challenge is based off—you don&amp;#39;t have to start from zero if you lose your progress (and the rules aren&amp;#39;t quite as over the top, in my opinion), so I&amp;#39;m confident that we&amp;#39;ll &lt;em&gt;at least&lt;/em&gt; reach the end. It&amp;#39;s an added bonus we can do it without missing any days!&lt;/p&gt;
&lt;p&gt;The rules are, helpfully, left slightly vague, and they allow you to attach your own, more specific goals to them, which I really like. This is what we&amp;#39;ve all agreed on:&lt;/p&gt;
&lt;h2&gt;🍱 Eat well&lt;/h2&gt;
&lt;p&gt;For my girlfriend and I, this means cooking at home whenever possible and reducing the amount times we eat out or get takeaway. I wouldn&amp;#39;t say we do this &lt;em&gt;a lot&lt;/em&gt; at the moment, but it&amp;#39;s an expensive habit to have in London and we often don&amp;#39;t even feel like it was money well spent after the fact, anyway. Committing to cooking more at home will almost certainly help us eat healthier and will &lt;em&gt;definitely&lt;/em&gt; help out our wallets. We don&amp;#39;t really drink alcohol at home, anyway, so that part won&amp;#39;t be hard.&lt;/p&gt;
&lt;h2&gt;🏃‍♂️ Exercise once a day&lt;/h2&gt;
&lt;p&gt;I used to be quite good at exercising regularly. I completed &lt;a href=&quot;https://www.nhs.uk/better-health/get-active/get-running-with-couch-to-5k/couch-to-5k-running-plan/&quot;&gt;Couch to 5k&lt;/a&gt; in the first half of 2024 and managed to keep a consistent habit through most of 2025 but was unfortunately thwarted due to winter (cold) and injuries (ouch). My plan is to alternate running and gym workouts to hopefully strengthen the muscles involved and ensure I can keep up the level of activity for the entire 2.5-month period. We&amp;#39;ve also agreed that 10,000 steps in a day is a worth substitute if we aren&amp;#39;t able to &amp;quot;properly&amp;quot; exercise for whatever reason.&lt;/p&gt;
&lt;h2&gt;🚰 Drink 3 litres of water a day&lt;/h2&gt;
&lt;p&gt;Not much here to say. I downloaded &lt;a href=&quot;https://dripmobile.app/&quot;&gt;an app&lt;/a&gt; to help me track how much water I&amp;#39;ve had. Fortunately, &lt;a href=&quot;https://www.nhs.uk/live-well/eat-well/food-guidelines-and-food-labels/water-drinks-nutrition/#:~:text=tea%20and%20coffee%2C%20all%20count%20as%20part%20of%20your%20daily%20intake&quot;&gt;&lt;strong&gt;tea and coffee also count!&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;📖 Read 10 pages a day&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://hardcover.app/@bens/stats&quot;&gt;I already read basically every day&lt;/a&gt; so this isn&amp;#39;t much of an additional to my daily routine. I will be reading &lt;a href=&quot;https://hardcover.app/books/dust-of-dreams&quot;&gt;&lt;em&gt;Dust of Dreams&lt;/em&gt;&lt;/a&gt; by Steven Erikson, and I&amp;#39;ve actually being a little naughty and committed to &lt;em&gt;17 pages&lt;/em&gt; (😱) a day so that the end of the book will nicely coincide with the end of 75 Soft. It&amp;#39;s definitely been a goal of mine to read &lt;em&gt;more&lt;/em&gt; each day, so I&amp;#39;m really happy this is part of the challenge.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I&amp;#39;m not really sure what to expect from this challenge, to be honest. I don&amp;#39;t believe it&amp;#39;ll be a silver bullet, life-changing experience that will somehow catapult me into the annals of greatness, but I definitely think I&amp;#39;ll feel proud to have completed it, and hopefully will have strengthened some healthy habits along the way.&lt;/p&gt;
&lt;p&gt;I remember feeling insanely proud when I completed Couch to 5k. There was no need for me to start it, other than a feeling that I &lt;em&gt;wanted&lt;/em&gt; to, and there was no goal for me other than to complete it. And sometimes that&amp;#39;s enough. We set ourselves arbitrary goals that we can work towards, and then we feel accomplished when we reach them!&lt;/p&gt;
&lt;p&gt;So with all this said, expect a follow-up in March!&lt;/p&gt;
</content:encoded></item><item><title>I sense another rewrite coming on</title><link>https://bensilverman.co.uk/posts/another-rewrite/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/another-rewrite/</guid><pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Feeling the itch to redo my website again. I’ve been getting increasingly interested in the whole IndieWeb thing and like the idea of really making this the home for all of my Stuff. I already have a page for the movies I’ve watched (see if you can find it) but that data still originates over on Letterboxd. The biggest problem I have with the whole POSSE (&lt;strong&gt;P&lt;/strong&gt;ublish on your &lt;strong&gt;O&lt;/strong&gt;wn &lt;strong&gt;S&lt;/strong&gt;ite, &lt;strong&gt;S&lt;/strong&gt;yndicate &lt;strong&gt;E&lt;/strong&gt;lsewhere) is that Letterboxd’s UI for writing my reviews, and reading others’, is just sooo much nicer. Unless you really lean in to all the ATProto things and shift your entire online self there (and convince everyone you care about to do so, too) then you’re kind of stuck using these third party services.&lt;/p&gt;
&lt;p&gt;There is an alternative acronym, PESOS, which is basically the same words but rearranged, and essentially amounts to taking a backup of your content and storing it on your own site. That’s what I do with my Letterboxd right now, with a terribly manual process involving exporting huge CSVs of all of my data every whenever-I-remember. There’s an RSS feed of my data I should probably hook up, actually, which might make this nicer to automate.&lt;/p&gt;
&lt;p&gt;I still haven’t found anything super great for tracking what books I’ve read. Everyone’s on Goodreads but I don’t like the Amazon association and also their app is actually quite ugly! I’m actually trying something an app called Margins right now but it may not last, given there’s no web version, nor an easy way to get my data out of the app.&lt;/p&gt;
&lt;p&gt;I think what is ultimately like is a way to write stuff on this website without it being a whole Thing, and for the stuff I write to be a bit more mixed in with the stuff I read, the stuff I watch, and the stuff I play. It could be fun to just have everything here and automatically synced from the other services I use.&lt;/p&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 1</title><link>https://bensilverman.co.uk/posts/aoc-25-1/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-1/</guid><pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Advent of Code is &lt;a href=&quot;https://www.reddit.com/r/adventofcode/comments/1ocwh04/changes_to_advent_of_code_starting_this_december/&quot;&gt;nearly over&lt;/a&gt; for this year and I thought I would try and do some quick write-ups of my solutions, mainly as a bit of fun and to help me improve my writing a little bit. So far it&amp;#39;s been quite a good level of difficulty, with nothing outstandingly horrendous, but a few which have been challenging. I don&amp;#39;t know whether the plan is to scale Days 1-12 in such a way that Day 12 is as difficult as Day 25 would&amp;#39;ve been in previous years. It&amp;#39;ll probably be similar?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h1&gt;Day 1&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/1&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Part 1&lt;/h2&gt;
&lt;p&gt;Quite a straightforward task: simulate a rotating dial on a safe, and count up how many times the dial lands at position &lt;code&gt;0&lt;/code&gt; after each turn.&lt;/p&gt;
&lt;p&gt;Just increment or decrement &lt;code&gt;rot&lt;/code&gt; depending on the input line, and &lt;code&gt;mod&lt;/code&gt; it to keep it within the 0-99 range specified.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const input = await readInput().then((data) =&amp;gt;
	Array.from(data.join(&amp;quot;\n&amp;quot;).matchAll(ROTATON_REGEX))
);

function partOne() {
	let rot = 50;
	let answer = 0;

	for (const [, direction, amount] of input) {
		switch (direction) {
			case &amp;quot;L&amp;quot;: {
				rot = mod(rot - Number(amount), 100);
				break;
			}
			case &amp;quot;R&amp;quot;: {
				rot = mod(rot + Number(amount), 100);
			}
		}

		if (rot === 0) answer += 1;
	}

	return answer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&amp;#39;s super fun that you actually need to write a custom &lt;code&gt;mod&lt;/code&gt; function instead of just using &lt;code&gt;%&lt;/code&gt;, like in a language like Python. This is because the &lt;code&gt;%&lt;/code&gt; operator in JavaScript doesn&amp;#39;t actually calculate the modulus of a number, but returns the &lt;em&gt;remainder&lt;/em&gt; instead, so something like &lt;code&gt;-3 % 4&lt;/code&gt; will actually return &lt;code&gt;-3&lt;/code&gt; instead of the &lt;code&gt;1&lt;/code&gt; that we want.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s a function you can use to get mods how you want them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const mod = (n: number, m: number) =&amp;gt;  ((n % m) + m) % m;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Part 2&lt;/h2&gt;
&lt;p&gt;Part 2 is a little bit more involved, and asks that you calculate the number of times the dial &lt;em&gt;passes&lt;/em&gt; &lt;code&gt;0&lt;/code&gt;, not just landing on it. I opted for the lazy approach and essentially just broke down my input into lots of single-step &amp;quot;left&amp;quot; and &amp;quot;right&amp;quot; turns. By doing this, you can basically then solve the problem the same as in Part 1.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function partTwo() {
	let rot = 50;
	let answer = 0;

	for (const [, direction, amount] of input) {
		switch (direction) {
			case &amp;quot;L&amp;quot;: {
				for (let i = 0; i &amp;lt; Number(amount); i++) {
					rot = mod(rot - 1, 100);

					if (rot === 0) answer += 1;
				}

				break;
			}
			case &amp;quot;R&amp;quot;: {
				for (let i = 0; i &amp;lt; Number(amount); i++) {
					rot = mod(rot + 1, 100);

					if (rot === 0) answer += 1;
				}
			}
		}
	}

	return answer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&amp;#39;s not very fast—about 4x slower than my Part 1 solution—but it&amp;#39;s very easy to implement. A better approach would probably be to look at where you land after the movement and work out whether you would&amp;#39;ve had to pass &lt;code&gt;0&lt;/code&gt; to get there.&lt;/p&gt;
&lt;h2&gt;Benchmarks&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;clk: ~3.07 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Part 1:                      121.41 µs/iter 121.00 µs  █                   
                    (117.79 µs … 224.92 µs) 146.00 µs  █▄                  
                    ( 32.00  b … 227.59 kb) 923.38  b ▂██▅▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

Part 2:                        5.21 ms/iter   5.24 ms   ▄▂█    ▂           
                        (5.13 ms … 5.42 ms)   5.35 ms  ▆███▃   █           
                    (504.00  b … 504.00  b) 504.00  b ▅██████▆▇██▇▄█▆▅▄▅▂▂▄
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 10</title><link>https://bensilverman.co.uk/posts/aoc-25-10/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-10/</guid><pubDate>Wed, 10 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A bit of linear programming on my birthday, how lovely!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/10&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Part 1&lt;/h2&gt;
&lt;p&gt;My idea for solving Part 1 was to represent the lights as a &lt;code&gt;Set&lt;/code&gt; of numbers, and then use the use newly available &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference&quot;&gt;&lt;code&gt;symmetricDifference&lt;/code&gt;&lt;/a&gt; method to toggle the lights on and off. Doing &lt;code&gt;setA.symmetricDifference(setB)&lt;/code&gt; will return numbers which are in either &lt;code&gt;setA&lt;/code&gt; or &lt;code&gt;setB&lt;/code&gt;, but not &lt;em&gt;both&lt;/em&gt;. So for a set of lights &lt;code&gt;lights := {1, 3, 4}&lt;/code&gt; we can turn off lights 1 and 4, and switch on light 2 by doing &lt;code&gt;lights.symmetricDifference({1, 2, 4})&lt;/code&gt; (this isn&amp;#39;t real JS notation but you get the picture).&lt;/p&gt;
&lt;p&gt;The next thing I did was write a &lt;a href=&quot;https://github.com/benslv/aoc/blob/master/2025/utils/combinations.ts&quot;&gt;&lt;code&gt;combinations&lt;/code&gt;&lt;/a&gt; generator to gradually yield all of the possible combinations of button presses. One thing to note is we don&amp;#39;t need to think about any combinations containing multiple of the same button wiring schematic, because they will effectively &amp;quot;cancel out&amp;quot; and can either be represented by a single occurrence, or zero occurrences (e.g. the symmetric difference between {1, 2} and {1, 2} is an empty set, which is the same as never having toggled those lights in the first place).&lt;/p&gt;
&lt;p&gt;So let&amp;#39;s generate all of the combinations of our available button press schematics, from the smallest combination (i.e. 1) to the largest, and break when we find a combination that leads to all of the lights being on. &lt;/p&gt;
&lt;h2&gt;Part 2&lt;/h2&gt;
&lt;p&gt;Suddenly these buttons start incrementing counters on a &lt;em&gt;different&lt;/em&gt; machine instead, and we have to also make sure these counters hit their required values too. It&amp;#39;s a lot trickier and will require a different approach.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re smarter than me you&amp;#39;ll probably very quickly recognise this as a &lt;a href=&quot;https://github.com/benslv/aoc/blob/master/2025/utils/combinations.ts&quot;&gt;Linear Programming&lt;/a&gt; question. I might revisit and write out an explanation in full (partly for my own benefit) but the gist is that we can represent the system of button presses as a matrix and the counter values as a vector, and then do some Maths to it to have it tell us what the minimum number of button presses required to hit the desired counter values is.&lt;/p&gt;
&lt;p&gt;I opted to use a &lt;a href=&quot;https://github.com/IanManske/YALPS&quot;&gt;Linear Programming library&lt;/a&gt; to &amp;quot;solve this for me&amp;quot;, which might be considered cheating by some people but I&amp;#39;m not sure I mind too much. That&amp;#39;s what tools and libraries are made for in the first place, right?&lt;/p&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;clk: ~3.03 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Part 1                        21.99 ms/iter  22.10 ms  █▂▂ ▂    ▂          
                      (21.75 ms … 22.61 ms)  22.44 ms  ███▅█  ▅ █     ▅    
                    ( 10.96 mb …  11.07 mb)  11.00 mb ▇█████▁▇█▁█▇▁▁▇▁█▁▁▁▇

Part 2                         1.55 ms/iter   1.56 ms  █                   
                        (1.35 ms … 2.41 ms)   2.17 ms ▂█▄                  
                    (  1.50 mb …   4.35 mb)   3.31 mb ███▅▃▃▂▁▁▁▁▁▂▃▃▃▄▂▃▂▁
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 11</title><link>https://bensilverman.co.uk/posts/aoc-25-11/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-11/</guid><pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Finally some more graph traversal!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/11&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Part 1&lt;/h2&gt;
&lt;p&gt;Simple breadth-first search for Part 1. We want to know how many routes are possible to get from the start to the finish.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function partOne() {
	const paths = [];

	const queue: Array&amp;lt;Array&amp;lt;string&amp;gt;&amp;gt; = [[&amp;quot;you&amp;quot;]];

	while (queue.length &amp;gt; 0) {
		const path = queue.shift()!;
		const node = path[path.length - 1];

		if (node === &amp;quot;out&amp;quot;) {
			paths.push(path);
			continue;
		}

		const children = nodes.get(node);

		for (const child of children ?? []) {
			queue.push([...path, child]);
		}
	}

	return paths.length;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Part 2&lt;/h2&gt;
&lt;p&gt;In Part 2, we start from &lt;code&gt;svr&lt;/code&gt; instead of &lt;code&gt;you&lt;/code&gt; and there are &lt;em&gt;so many&lt;/em&gt; more potential paths that could be visited. We are also only interested in the paths that pass through &lt;code&gt;dac&lt;/code&gt; and &lt;code&gt;fft&lt;/code&gt;, too, so a bit of added complication there.&lt;/p&gt;
&lt;p&gt;There will be many moments where we are traversing from the same point over and over again, so let&amp;#39;s introduce some memoization to cache the number of valid routes (i.e. passing through &lt;code&gt;dac&lt;/code&gt; and &lt;code&gt;fft&lt;/code&gt;) from a given point to the finish so we can reuse the value the next time we see it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function partTwo(): number {
	const countPaths = memoize(
		(here: string, dac: boolean, fft: boolean): number =&amp;gt; {
			let dacVisited = dac;
			let fftVisited = fft;

			switch (here) {
				case &amp;quot;out&amp;quot;:
					return Number(dac &amp;amp;&amp;amp; fft);
				case &amp;quot;dac&amp;quot;:
					dacVisited = true;
					break;
				case &amp;quot;fft&amp;quot;:
					fftVisited = true;
					break;
			}

			const result = Array.from(nodes.get(here) ?? []).reduce(
				(total, v) =&amp;gt; total + countPaths(v, dacVisited, fftVisited),
				0
			);

			return result;
		}
	);

	return countPaths(&amp;quot;svr&amp;quot;, false, false);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we see that we&amp;#39;re at &lt;code&gt;out&lt;/code&gt; we can return &lt;code&gt;Number(dac &amp;amp;&amp;amp; fft)&lt;/code&gt; so the route only counts towards the total number of valid routes if &lt;em&gt;both&lt;/em&gt; required points have been visited. Otherwise, the route will be a &lt;code&gt;0&lt;/code&gt; and not count towards the total.&lt;/p&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;clk: ~3.03 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Part 1                        21.99 ms/iter  22.10 ms  █▂▂ ▂    ▂          
                      (21.75 ms … 22.61 ms)  22.44 ms  ███▅█  ▅ █     ▅    
                    ( 10.96 mb …  11.07 mb)  11.00 mb ▇█████▁▇█▁█▇▁▁▇▁█▁▁▁▇

Part 2                         1.55 ms/iter   1.56 ms  █                   
                        (1.35 ms … 2.41 ms)   2.17 ms ▂█▄                  
                    (  1.50 mb …   4.35 mb)   3.31 mb ███▅▃▃▂▁▁▁▁▁▂▃▃▃▄▂▃▂▁
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 2</title><link>https://bensilverman.co.uk/posts/aoc-25-2/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-2/</guid><pubDate>Tue, 02 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Thank god I know how to regex...otherwise this would&amp;#39;ve been painful.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/2&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Part 1&lt;/h1&gt;
&lt;p&gt;Part 1 of Day 2 asks for us to take an interval and calculate how many numbers within that interval are made up of some sequence of digits repeated exactly twice (e.g. &lt;code&gt;11&lt;/code&gt; and &lt;code&gt;22&lt;/code&gt; in the range &lt;code&gt;11-22&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { bench, run } from &amp;quot;mitata&amp;quot;;
import { readInput } from &amp;quot;../utils&amp;quot;;

const P1_REGEX = /^(\d+)\1$/;

const input = await readInput();

const ranges = input
	.at(0)
	.split(&amp;quot;,&amp;quot;)
	.map((r) =&amp;gt; r.split(&amp;quot;-&amp;quot;).map(Number));

let partOne = 0;

for (const [start, end] of ranges) {
	for (let i = start; i &amp;lt;= end; i++) {
		if (P1_REGEX.test(i.toString())) partOne += i;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The star of this solution is the regex, &lt;code&gt;^(\d+)\1$&lt;/code&gt;, which does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;^&lt;/code&gt; - matches the beginning of the string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(\d+)&lt;/code&gt; - creates a &amp;quot;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Capturing_group&quot;&gt;capture group&lt;/a&gt;&amp;quot; around one or more digit characters&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\1&lt;/code&gt; - is a &amp;quot;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Backreference&quot;&gt;back reference&lt;/a&gt;&amp;quot; to the first capture group we created&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$&lt;/code&gt; - matches the end of the string&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using &lt;code&gt;^&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; ensure we don&amp;#39;t match any substrings containing repeating sequences of digits (e.g. &lt;code&gt;123231&lt;/code&gt;)!&lt;/p&gt;
&lt;h1&gt;Part 2&lt;/h1&gt;
&lt;p&gt;The problem statement then changes very slightly for Part 2, and instead asks us to find all of the numbers within a given interval which are made up of a sequence of digits that repeats &lt;em&gt;at least&lt;/em&gt; twice (e.g. &lt;code&gt;343434&lt;/code&gt;, &lt;code&gt;1111&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;This would be really horrible, if not for that fact that we can reuse basically all of our Part 1 solution just by changing the regex slightly.&lt;/p&gt;
&lt;p&gt;✨ &lt;code&gt;^(\d+)\1$&lt;/code&gt; becomes &lt;code&gt;^(\d+)\1+$&lt;/code&gt; ✨&lt;/p&gt;
&lt;p&gt;With this new regex we can create the capture group and then match strings with &lt;em&gt;one or more&lt;/em&gt; of the back references, instead of exactly one.&lt;/p&gt;
&lt;h1&gt;Benchmark&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;clk: ~2.93 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Part 1 &amp;amp; 2                   266.81 ms/iter 264.07 ms  ▂█                  
                    (262.53 ms … 292.19 ms) 274.98 ms ▅██                  
                    (  1.02 mb …  16.50 mb)   8.06 mb ███▇▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▇
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I haven&amp;#39;t tried this out yet but you could write this same test using purely mathematical operations instead of a regex and it might be faster? Something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function testNumber(n: number): boolean {
	const numDigits = Math.floor(Math.log10(n) + 1);

	// Needs to have an even number of digits to split cleanly.
	if (numDigits % 2 !== 0) return false;

	const firstHalf = Math.floor(n / Math.pow(10, numDigits / 2))
	const secondHalf = n % Math.pow(10, numDigits / 2)

	return firstHalf === secondHalf
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 3</title><link>https://bensilverman.co.uk/posts/aoc-25-3/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-3/</guid><pubDate>Wed, 03 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;An appearance from the Duracell Bunny today, as we compare batteries!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/3&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Part 1&lt;/h2&gt;
&lt;p&gt;My approach for today was to split up each line of input into an array of digits and then scan through each array, gradually selecting the next largest number available to produce the greatest final result.&lt;/p&gt;
&lt;p&gt;We store the indices of each largest digit of the number to allow us to skip over sections of the bank and not waste time searching batteries which wouldn&amp;#39;t be valid anyway. Given we know the number of batteries that need to be switched on at the end—parameter &lt;code&gt;n&lt;/code&gt; in our function—we can also cut off the search early to ensure there are enough batteries left over for the next spots in the bank.&lt;/p&gt;
&lt;p&gt;For example, given a bank of 10 batteries where we want to switch on 6 to make the highest number, we know that we can&amp;#39;t search any further than index 4 for the first battery, because the remaining 5 batteries in the bank would all then need to be switched on.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;             b1 b2 b3 b4 b5 b6 
             ↓
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function partOne() {
	return input.reduce(
		(total, bank) =&amp;gt; total + findLargestJoltageForNBatteries(bank, 2),
		0
	);
}

function findLargestJoltageForNBatteries(
	bank: Array&amp;lt;number&amp;gt;,
	n: number
): number {
	const array: number[] = [];

	for (let i = 0; i &amp;lt; n; i++) {
		const prevIndex = i === 0 ? 0 : array[i - 1] + 1;

		array.push(maxIndex(bank, prevIndex, bank.length - (n - (i + 1))));
	}

	return array.reduce((acc, val) =&amp;gt; 10 * acc + bank[val], 0);
}

function maxIndex(array: Array&amp;lt;number&amp;gt;, start: number, end: number): number {
	let m = start;

	for (let i = start; i &amp;lt; end; i++) {
		if (array[i] &amp;gt; array[m]) m = i;
	}

	return m;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Part 2&lt;/h2&gt;
&lt;p&gt;We&amp;#39;re just doing the same thing here, but looking for 12 digits instead of just 2. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function partTwo() {
	return input.reduce(
		(total, bank) =&amp;gt; total + findLargestJoltageForNBatteries(bank, 12),
		0
	);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;clk: ~3.03 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Part 1                        43.91 µs/iter  43.54 µs  █                   
                     (42.29 µs … 267.50 µs)  55.13 µs ▂█                   
                    (  2.43 kb … 262.59 kb)  42.15 kb ███▄▃▃▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁

Part 2                        84.91 µs/iter  84.08 µs  █                   
                     (80.50 µs … 950.75 µs) 120.00 µs ▂█                   
                    (  6.71 kb … 220.11 kb)  37.54 kb ███▄▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing I wondered about while writing this was how these benchmarks might scale in relation to the number of batteries that need to be switched on. As the number of batteries being switched on approaches the total number of batteries in the bank, the number of actual searches we can do should reduce because there are fewer numbers that are possible within the &amp;quot;space&amp;quot;. For example, if we have a battery bank of 10 batteries and we want to switch on all 10, there&amp;#39;s only one possible order in which they can be switched on (given the constraint that the order of the batteries can&amp;#39;t change).&lt;/p&gt;
&lt;p&gt;But that&amp;#39;s not what I see when I benchmark my code. Our AoC input consists of a list of numbers with 100 digits, so we can benchmark all the way up to that amount and get the following graph:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;------------------------------------------- -------------------------------
1                             68.69 µs/iter  68.42 µs █                    
                     (66.96 µs … 277.21 µs)  83.79 µs █ ▃                  
                    ( 23.09 kb … 333.22 kb)  52.02 kb █▅█▂▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

8                            202.71 µs/iter 201.58 µs █                    
                    (199.88 µs … 407.21 µs) 243.08 µs █▃                   
                    (106.38 kb … 543.90 kb) 106.85 kb ██▅▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

64                           460.29 µs/iter 452.46 µs █                    
                    (447.33 µs … 743.17 µs) 576.17 µs █▂                   
                    (852.63 kb …   1.51 mb)   1.24 mb ██▃▂▁▁▁▁▁▁▁▁▁▁▂▂▂▁▁▁▁

100                          608.57 µs/iter 597.13 µs █                    
                    (588.54 µs … 813.67 µs) 721.88 µs █▇                   
                    (  2.31 mb …   2.31 mb)   2.31 mb ██▂▂▂▁▁▁▁▁▁▁▁▁▃▄▂▁▁▁▁

                             ┌                                            ┐
                        $num                                           ⡠⠔⠊ 608.57 µs
                                                                   ⢀⠤⠒⠉   
                                                                ⣀⠔⠊⠁      
                                                            ⢀⡠⠒⠉          
                                                          ⡠⠊⠁             
                                                        ⡠⠊                
                                                      ⡠⠊                  
                                                    ⡠⠊                    
                                                  ⢀⠜                      
                                                ⢀⠔⠁                       
                                              ⢀⠔⠁                         
                                            ⢀⠔⠁                           
                                        ⢀⡠⠔⠊⠁                             
                                     ⣀⠤⠒⠁                                 
                                 ⢀⡠⠒⠉                                     
                              ⡠⠔⠊⠁                                         68.69 µs
                             └                                            ┘

summary
  1
   2.95x faster than 8
   6.7x faster than 64
   8.86x faster than 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The time taken to run seems to scale linearly with the number of batteries being switched on, which is not what I expected. Something to think about...&lt;/p&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 4</title><link>https://bensilverman.co.uk/posts/aoc-25-4/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-4/</guid><pubDate>Thu, 04 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Toilet paper meets Sokoban...sort of. We have to look through all of the rolls of toilet paper on a grid and work out which can be removed, which will be when there are fewer than 4 rolls of toilet paper in the adjacent positions around each roll.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/4&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Part 1&lt;/h2&gt;
&lt;p&gt;I didn&amp;#39;t really have any clever solution for this. I just iterated through each roll in the grid and counted how many adjacent spaces to it contained a roll, and incremented my answer if that value &amp;lt; 4.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function partOne() {
	const grid = new Grid(input);

	let answer = 0;

	for (const [pos, val] of grid.points.entries()) {
		if (val !== &amp;quot;@&amp;quot;) continue;

		const [y, x] = pos.split(&amp;quot;,&amp;quot;).map(Number);

		let nearbyRolls = 0;

		for (const dy of [-1, 0, 1]) {
			for (const dx of [-1, 0, 1]) {
				if (dy === 0 &amp;amp;&amp;amp; dx === 0) continue;

				if (grid.get(y + dy, x + dx) === &amp;quot;@&amp;quot;) {
					nearbyRolls += 1;
				}
			}
		}

		if (nearbyRolls &amp;lt; 4) {
			answer += 1;
		}
	}

	return answer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I was able to use my custom &lt;a href=&quot;https://github.com/benslv/aoc/blob/master/2025/utils/Grid.ts&quot;&gt;Grid&lt;/a&gt; helper from last year, which was nice. All it really does it parse the input into a Map of points that can then be queried. There are some benefits to this, like not running into errors when accessed nested arrays that might be out of bounds—the Grid will just return &lt;code&gt;undefined&lt;/code&gt; instead. &lt;/p&gt;
&lt;p&gt;It sucks having to format the keys as strings, though. In an ideal world you could use something like a tuple, but JavaScript doesn&amp;#39;t have those and you can&amp;#39;t use an array as a key because it&amp;#39;s added as a reference, which means you can&amp;#39;t construct these keys on the fly...something you might want to do if you need to, say, check all of the adjacent points to a coordinate.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachments/10090.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Part 2&lt;/h2&gt;
&lt;p&gt;I &lt;em&gt;also&lt;/em&gt; didn&amp;#39;t do anything clever for Part 2. I fancied my luck just bruteforcing through to the end and it turned out to be enough, so I was happy to stop there. We can basically just wrap the Part 1 solution in a big ol&amp;#39; &lt;code&gt;do/while&lt;/code&gt; loop and repeat until there are no rolls left on the grid. It&amp;#39;s not very pretty but it&amp;#39;s also not very fast!&lt;/p&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;clk: ~3.01 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Part 1                        10.75 ms/iter  10.83 ms       █              
                      (10.10 ms … 12.59 ms)  12.09 ms  ▃   ▂█▇             
                    (  1.76 mb …  11.96 mb)  11.41 mb ▇█▂▄▂███▅▅▁▁▁▂▂▂▂▂▁▂▂

Part 2                       288.44 ms/iter 292.85 ms █   █                
                    (279.43 ms … 307.79 ms) 299.67 ms █▅▅ █▅    ▅  ▅     ▅▅
                    ( 14.39 mb …  14.39 mb)  14.39 mb ███▁██▁▁▁▁█▁▁█▁▁▁▁▁██
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 5</title><link>https://bensilverman.co.uk/posts/aoc-25-5/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-5/</guid><pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Ingredient management! We need to figure out which ingredients are fresh, and which aren&amp;#39;t.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/5&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Part 1&lt;/h2&gt;
&lt;p&gt;It&amp;#39;s easy enough to write a bruteforce solution for Part 1 and check every ingredient to see if it lies within one of the given ranges of fresh ingredients.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const freshRanges = ranges.map&amp;lt;Range&amp;gt;(r =&amp;gt; { const [lower, upper] = r.split(&amp;quot;-&amp;quot;).map(Number); return { lower, upper } });

function partOne() {
	let answer = 0;

	for (const ing of ingredients) {
		for (const r of freshRanges) {
			if (Number(ing) &amp;gt;= r.lower &amp;amp;&amp;amp; Number(ing) &amp;lt;= r.upper) {
				answer += 1
				break;
			}
		}
	}

	return answer
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Part 2&lt;/h2&gt;
&lt;p&gt;Now the challenge shifts slightly: rather than checking which of our ingredients are fresh, we have to look at all of our ranges and work out the total number of ingredients that are considered &amp;quot;fresh&amp;quot;.&lt;/p&gt;
&lt;p&gt;I also initially tried to bruteforce this and just iterate through each range, adding the ingredients to a set along the way. The idea is that, by the end of the final ingredient range, the set has collected every fresh ingredient exactly once and we can use the size of the set as our answer.&lt;/p&gt;
&lt;p&gt;Obviously, it wasn&amp;#39;t that simple, though. It turns out that these ranges are quite big and you very quickly exceed the set&amp;#39;s maximum size.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachments/537.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Oops...&lt;/p&gt;
&lt;p&gt;The approach I went for after this was to sort the ranges in ascending order by their starting value, and then iterate through the list of ranges and increment my final answer by &lt;code&gt;range.upper - range.lower&lt;/code&gt;. We can use a &amp;quot;pointer&amp;quot; of sorts to keep track of the highest value seen so far, and use it in that calculation to resolve any overlaps.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function partTwo() {
	let answer = 0;
	let curr = 0;

	for (const range of sortedRanges) {
		let start = Math.max(range.lower, curr + 1);
		answer += Math.max(0, range.upper - start + 1);
		curr = Math.max(curr, range.upper);
	}

	return answer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;attachments/Diagram.jpg&quot; alt=&quot;&quot;&gt;
Ooh, a drawing! After counting the first range (1-4) the &lt;code&gt;curr&lt;/code&gt; pointer will be at 4, so we start from there and calculate &lt;code&gt;8 - 4&lt;/code&gt; to skip the overlap and count the remaining numbers in the second range.&lt;/p&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;p&gt;Don&amp;#39;t you love when Part 2 runs faster than Part 1 🙈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clk: ~3.11 GHz
cpu: Apple M1 Pro
runtime: node 20.17.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Part 1                         8.32 ms/iter   8.35 ms  █                   
                        (8.22 ms … 8.80 ms)   8.68 ms ▃█▃                  
                    (  2.74 mb …   2.74 mb)   2.74 mb ███▅▅▄▃▅▃▃▁▁▂▃▂▃▁▁▁▂▃

Part 2                       249.24 ns/iter 249.21 ns  █                   
                    (246.82 ns … 310.57 ns) 271.93 ns ▆█                   
                    (  4.17  b … 972.03  b)   6.59  b ██▇▄▄▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 6</title><link>https://bensilverman.co.uk/posts/aoc-25-6/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-6/</guid><pubDate>Sat, 06 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Oh how I envy Python&amp;#39;s &lt;code&gt;zip()&lt;/code&gt; function...&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/6&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Part 1&lt;/h2&gt;
&lt;p&gt;I think today&amp;#39;s trickiness was mainly from figuring out what the question wanted you to do, and how it code something that Did It Correctly.&lt;/p&gt;
&lt;p&gt;Unfortunately, JS doesn&amp;#39;t have a great standard library of array functions for stuff like this, so I resorted to writing my own &lt;code&gt;transpose()&lt;/code&gt; function and using it to swap the rows and the columns of my input.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function transpose(matrix: number[][]): number[][] {
	const rows = matrix.length;

	const cols = matrix[0].length;
	const grid: number[][] = [];

	for (let j = 0; j &amp;lt; cols; j++) {
		grid.push([]);
		for (let i = 0; i &amp;lt; rows; i++) {
			grid[j].push(matrix[i][j]);
		}
	}

	return grid;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can then reduce down our list of columns, combining each one using  the operator that&amp;#39;s provided—adding or multiplying—and sum everything up to get our final answer!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const funcMap: Record&amp;lt;string, (a: number, b: number) =&amp;gt; number&amp;gt; = {
	&amp;quot;+&amp;quot;: (a: number, b: number) =&amp;gt; a + b,
	&amp;quot;*&amp;quot;: (a: number, b: number) =&amp;gt; a * b,
};

function partOne() {
	const rows = input
		.slice(0, -1)
		.map((row) =&amp;gt; row.trim().split(/\s+/).map(Number));

	const ops = input.at(-1)!.trim().split(/\s+/);

	return transpose(rows).reduce(
		(acc, row, i) =&amp;gt; acc + row.reduce(funcMap[ops[i]]),
		0
	);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Part 2&lt;/h2&gt;
&lt;p&gt;Unfortunately I couldn&amp;#39;t use that same &lt;code&gt;transpose&lt;/code&gt; function for the second part of Day 6. We still need to process the groups of numbers in their columns, but now we &lt;em&gt;also&lt;/em&gt; need to read the numbers in right-to-left, &lt;em&gt;and&lt;/em&gt; in columns.&lt;/p&gt;
&lt;p&gt;So something like...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;123
 45
  6
+  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...would be &lt;code&gt;356 + 24 + 1 = 381&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I opted to do a single scan from right to left of the entire input and process the columns as I went. We can create a temporary list of items by reading in all of the characters from a given column of input. Eventually we will see a character like &lt;code&gt;+&lt;/code&gt; or &lt;code&gt;*&lt;/code&gt;, too, and can store that as the operator needed for the calculation. And then, when we read in an entire column of empty strings, we know we&amp;#39;ve fully processed a single &amp;quot;group&amp;quot; of numbers and can add that to the final answer.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function partTwo() {
	const numCols = input[0].length;
	const numRows = input.length;

	const transposedInput: number[][] = [[]];
	const ops: string[] = [];

	for (let i = 0; i &amp;lt; numCols; i++) {
		const items = [];

		for (let j = 0; j &amp;lt; numRows; j++) {
			items.unshift(input[j][i]);
		}

		const [op, ...nums] = items;

		const num = nums.toReversed().join(&amp;quot;&amp;quot;).trim();

		if (num.length === 0) {
			transposedInput.push([]);
			continue;
		}

		if (op !== &amp;quot; &amp;quot;) {
			ops.push(op);
		}

		transposedInput.at(-1)!.push(Number(num));
	}

	return transposedInput.reduce(
		(acc, nums, i) =&amp;gt; acc + nums.reduce(funcMap[ops[i]]),
		0
	);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;clk: ~3.00 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Part 1                       293.36 µs/iter 295.63 µs █                    
                    (275.21 µs … 557.38 µs) 441.21 µs █▅▅                  
                    ( 54.63 kb …   1.19 mb) 532.01 kb ████▄▂▁▂▂▁▁▁▁▁▁▁▁▁▁▁▁

Part 2                       893.25 µs/iter 888.83 µs █                    
                      (860.92 µs … 1.26 ms)   1.09 ms █                    
                    (  1.25 mb …   3.01 mb)   2.11 mb ██▆▃▂▂▂▁▁▂▂▂▂▂▂▁▁▁▁▁▁
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 7</title><link>https://bensilverman.co.uk/posts/aoc-25-7/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-7/</guid><pubDate>Sun, 07 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I think this was my favourite day of Advent of Code this year. Very interesting puzzle with some super satisfying solutions once you give it a bit of thought (or a lot of thought, in my case).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/7&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Part 1&lt;/h2&gt;
&lt;p&gt;So we&amp;#39;ve got a beam that&amp;#39;s being fired down through a Christmas tree-shaped structure of &amp;quot;beam splitters&amp;quot; and we need to count how many times the beam gets split as it makes its way down the grid.&lt;/p&gt;
&lt;p&gt;I simulated the beam&amp;#39;s journey down through the grid, line by line, by checking the character at each square of the grid and modifying the characters on the next line (&amp;quot;below&amp;quot; it) depending on whether it was a beam (&lt;code&gt;|&lt;/code&gt;) or a splitter (&lt;code&gt;^&lt;/code&gt;). Every time we hit a splitter we can increment a counter that tracks our final answer.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function partOne(input: string[]) {
	const grid: (string | number)[][] = input.map((row) =&amp;gt; row.split(&amp;quot;&amp;quot;));
	const startX = grid[0].indexOf(&amp;quot;S&amp;quot;);

	grid[1][startX] = &amp;quot;|&amp;quot;;

	let partOne = 0;

	for (const [j, row] of grid.slice(0, -1).entries()) {
		for (const [i, char] of row.entries()) {
			if (char !== &amp;quot;|&amp;quot;) continue;

			if (grid[j + 1][i] === &amp;quot;^&amp;quot;) {
				grid[j + 1][i - 1] = &amp;quot;|&amp;quot;;
				grid[j + 1][i + 1] = &amp;quot;|&amp;quot;;

				partOne += 1;
			} else {
				grid[j + 1][i] = &amp;quot;|&amp;quot;;
			}
		}
	}

	grid[1][startX] = 1;

	return [partOne, grid] as const;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I don&amp;#39;t think it was strictly necessary to simulate like this, and I probably could&amp;#39;ve stored the coordinates of the splitters and the beam positions, but this was easier (and more fun!) to visualise in my head. It also means we get a lovely visualisation by the end!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.......S.......
.......|.......
......|^|......
......|.|......
.....|^|^|.....
.....|.|.|.....
....|^|^|^|....
....|.|.|.|....
...|^|^|||^|...
...|.|.|||.|...
..|^|^|||^|^|..
..|.|.|||.|.|..
.|^|||^||.||^|.
.|.|||.||.||.|.
|^|^|^|^|^|||^|
|.|.|.|.|.|||.|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This then leads us very nicely into Part 2...&lt;/p&gt;
&lt;h2&gt;Part 2&lt;/h2&gt;
&lt;p&gt;The instructions for Part 2 were a little confusing to understand at first, but it boils down to &amp;quot;how many possible paths are there for a beam to take through this tree&amp;quot;. If you think about it, every time a beam hits a splitter it essentially creates two copies of itself, either side of the splitter, which continue on downwards. Those positions either side of the splitter could &lt;em&gt;also&lt;/em&gt; have beams arriving from directly above, or from the opposite side if there are two splitters next to each other. This is the perfect opportunity for some DP (Dynamic Programming 🤨), and also let&amp;#39;s us use the result from Part 1, since it effectively represents all of the possible paths the beam can take.&lt;/p&gt;
&lt;p&gt;The idea is to work downwards through the grid and add together the number of possible paths from above at each point, to track, by the end, the total number of possible paths that could&amp;#39;ve been taken by the beam. It took my quite a while to figure this out and properly understand it but I think it&amp;#39;s so cool that you can do this and it really doesn&amp;#39;t take too long to compute, either! &lt;/p&gt;
&lt;p&gt;I don&amp;#39;t know if I explained that very well so here&amp;#39;s a diagram that maybe illustrates it better.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.....1^2^1.....
.....1.2.1.....
....1^3^3^1....  &amp;lt;-- there are three ways to reach the two positions in the middle here
....1.3.3.1....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I don&amp;#39;t know if that &lt;em&gt;diagram&lt;/em&gt; explained that very well so here&amp;#39;s the actual code and maybe &lt;em&gt;that&lt;/em&gt; will illustrate it better.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function partTwo(grid: (string | number)[][]) {
	for (const [j, row] of grid.entries()) {
		for (const [i, char] of row.entries()) {
			if (char !== &amp;quot;|&amp;quot;) continue;

			grid[j][i] = getPointValue(grid, j - 1, i);

			if (grid[j][i - 1] === &amp;quot;^&amp;quot;) {
				grid[j][i] += getPointValue(grid, j - 1, i - 1);
			}

			if (grid[j][i + 1] === &amp;quot;^&amp;quot;) {
				grid[j][i] += getPointValue(grid, j - 1, i + 1);
			}
		}
	}

	return grid
		.at(-1)
		?.filter((n) =&amp;gt; typeof n === &amp;quot;number&amp;quot;)
		.reduce((acc, val) =&amp;gt; acc + val);
}

function getPointValue(
	grid: (string | number)[][],
	y: number,
	x: number
): number {
	return typeof grid[y][x] === &amp;quot;string&amp;quot; ? 0 : grid[y][x];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;clk: ~3.00 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Part 1                       293.36 µs/iter 295.63 µs █                    
                    (275.21 µs … 557.38 µs) 441.21 µs █▅▅                  
                    ( 54.63 kb …   1.19 mb) 532.01 kb ████▄▂▁▂▂▁▁▁▁▁▁▁▁▁▁▁▁

Part 2                       893.25 µs/iter 888.83 µs █                    
                      (860.92 µs … 1.26 ms)   1.09 ms █                    
                    (  1.25 mb …   3.01 mb)   2.11 mb ██▆▃▂▂▂▁▁▂▂▂▂▂▂▁▁▁▁▁▁
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 8</title><link>https://bensilverman.co.uk/posts/aoc-25-8/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-8/</guid><pubDate>Mon, 08 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Coincidentally, also the day I put my &lt;em&gt;own&lt;/em&gt; Christmas lights up!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/8&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Part 1 &amp;amp; 2&lt;/h2&gt;
&lt;p&gt;I&amp;#39;ll cover both parts at the same time today because you basically just need to do the same thing for each, but Part 2 requires you to do it for longer.&lt;/p&gt;
&lt;p&gt;To summarise today&amp;#39;s problem, you&amp;#39;re given a list of Christmas light IDs, and a map of which lights connect to which. When you connect two lights together, they form a &lt;strong&gt;circuit&lt;/strong&gt;. If two lights are already in a circuit when you connect them, the two circuits &lt;strong&gt;merge&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Straight away my mind started thinking about sets and unioning them together. We just need to do this over and over again, deleting old circuits once they&amp;#39;ve been connected together. We can track how many times we do this, and print out our answer for Part 1, then continue until there&amp;#39;s only one circuit left, at which point with have our Part 2 answer :)&lt;/p&gt;
&lt;p&gt;You can view &lt;a href=&quot;&quot;&gt;the full solution&lt;/a&gt; on my GitHub, because it&amp;#39;s quite long to post here, but the main part is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;for (const c of circuits) {
	if (c.has(a)) aCircuit = c;
	if (c.has(b)) bCircuit = c;
}

circuits.delete(aCircuit!);
circuits.delete(bCircuit!);

circuits.add(aCircuit!.union(bCircuit!));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This finds the circuits that contain the two lights (&lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;) which need to be merged, then combines them together into one mega-circuit and deletes the two smaller circuits.&lt;/p&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;clk: ~2.93 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Part 1 &amp;amp; 2                    14.72 µs/iter  14.42 µs   █                  
                       (12.79 µs … 1.17 ms)  20.83 µs   █▇▂                
                    (656.00  b … 152.39 kb)  23.00 kb ▁▆███▅▂▂▂▂▂▂▂▂▁▁▁▁▁▁▁
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a bit misleading, because it&amp;#39;s actually much slower if we include the time taken to sort all the points by their distance from each other :^)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clk: ~3.02 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                     avg (min … max) p75 / p99    (min … top 1%)
--------------------------------------------- -------------------------------
Part 1 &amp;amp; 2 (+sorting)          542.97 ms/iter 543.80 ms     █                
                      (536.48 ms … 567.32 ms) 545.21 ms ▅   █▅ ▅  ▅▅▅    ▅▅ ▅
                      ( 13.67 mb …  13.67 mb)  13.67 mb █▁▁▁██▁█▁▁███▁▁▁▁██▁█
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But we can leave that out of the official benchmark and chalk it up to &amp;quot;preparation time&amp;quot; 😉&lt;/p&gt;
</content:encoded></item><item><title>Advent of Code 2025 - Day 9</title><link>https://bensilverman.co.uk/posts/aoc-25-9/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/aoc-25-9/</guid><pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The hardest Part 2, I think!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventofcode.com/2025/day/9&quot;&gt;Read the full prompt.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Part 1&lt;/h2&gt;
&lt;p&gt;The idea is we have a bunch of points on a grid, and need to find which pair of points would create the largest rectangle (imagine them defining opposite corners of a rectangle).&lt;/p&gt;
&lt;p&gt;The first thing that tripped me up was realising that these coordinates weren&amp;#39;t on a continuous plane, and we needed to take into account the &amp;quot;size&amp;quot; of the point itself when calculating the area of a rectangle. This amounted to adding &lt;code&gt;1&lt;/code&gt; to each dimension when calculating the area.&lt;/p&gt;
&lt;p&gt;Then you just need to loop through each pair of points—you can cut out some duplicate checks by properly defining your start and ends of the &lt;code&gt;for&lt;/code&gt;-loops—and compare areas until you find the max.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;for (let i = 0; i &amp;lt; points.length - 1; i++) {
	const [x1, y1] = points[i];

	for (let j = i + 1; j &amp;lt; points.length; j++) {
		const [x2, y2] = points[j];

		const rMinX = Math.min(x1, x2);
		const rMaxX = Math.max(x1, x2);
		const rMinY = Math.min(y1, y2);
		const rMaxY = Math.max(y1, y2);

		const area = (rMaxY - rMinY + 1) * (rMaxX - rMinX + 1);

		partOne = Math.max(partOne, area);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Part 2&lt;/h2&gt;
&lt;p&gt;This got trickier in Part 2, however. These same coordinates now need to be treated as the &lt;em&gt;vertices&lt;/em&gt; of a bounding shape, and the only valid rectangles are ones which lie completely within this shape. I needed to turn to the &lt;a href=&quot;https://www.reddit.com/r/adventofcode&quot;&gt;Advent of Code subreddit&lt;/a&gt; for a bit of help on this one, I&amp;#39;ll admit. I &lt;em&gt;think&lt;/em&gt; I understand it now?&lt;/p&gt;
&lt;p&gt;It&amp;#39;s worth noting that the input file is defined in such a way that the next point in the list is connected to the one before it, all the way back to the beginning. This is how the boundary shape is defined, and it&amp;#39;s also something I missed at first because I skimmed through Part 2 and didn&amp;#39;t properly read it. Once we know this, we can construct a &lt;a href=&quot;https://docs.python.org/3/library/itertools.html#itertools.pairwise&quot;&gt;pairwise&lt;/a&gt; list of points to get the actual &lt;em&gt;edges&lt;/em&gt; of the bounding shape.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Coordinates are defined &amp;quot;in order&amp;quot; around the polygon, so we can do this.
for (let i = 0; i &amp;lt; points.length; i++) {
	greenLines.push([points[i], points[(i + 1) % points.length]]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For each rectangle we construct, then, we just need to check that it doesn&amp;#39;t intersect with any of these lines, and we know that it lies within the shape and can be considered valid. This is where my understanding falls apart a bit, though, because apparently this wouldn&amp;#39;t work as a general case. We&amp;#39;re lucky that Eric Wastl (the creator) generated an input that was okay! I think the idea is that this collision-detection wouldn&amp;#39;t work if there were any points that were directly adjacent to each other in the grid. One to look back at later, maybe.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;if (area &amp;gt; partTwo) {
	let isColliding = false;

	for (const [[p, q], [r, s]] of greenLines) {
		const eMinX = Math.min(p, r);
		const eMaxX = Math.max(p, r);
		const eMinY = Math.min(q, s);
		const eMaxY = Math.max(q, s);

		if (
			rMinX &amp;lt; eMaxX &amp;amp;&amp;amp;
			rMaxX &amp;gt; eMinX &amp;amp;&amp;amp;
			rMinY &amp;lt; eMaxY &amp;amp;&amp;amp;
			rMaxY &amp;gt; eMinY
		) {
			isColliding = true;
			break;
		}
	}

	if (!isColliding) {
		partTwo = Math.max(partTwo, area);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;clk: ~3.03 GHz
cpu: Apple M1 Pro
runtime: node 22.16.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
Both                         182.99 ms/iter 186.98 ms     █                
                    (170.26 ms … 199.06 ms) 198.78 ms     █                
                    (  5.83 mb …   5.83 mb)   5.83 mb █▁▁▁█████▁▁▁█▁▁▁▁█▁▁█
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Developing browser extensions seems hard</title><link>https://bensilverman.co.uk/posts/browser-extensions/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/browser-extensions/</guid><pubDate>Thu, 14 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&amp;#39;m building myself a bookmarks/read-it-later website to replace Instapaper and one requirements is saving links to it via a browser extension. (I also need to write an iOS app but that&amp;#39;s another kettle of fish).&lt;/p&gt;
&lt;p&gt;I didn&amp;#39;t think it would be too bad: it&amp;#39;s just JavaScript right? The problem lies in that there doesn&amp;#39;t seem to be any form of intellisense for the WebExtensions API, so I have to spend half my time flicking back and forth between VSCode and the MDN docs to figure out what methods are available. Additionally, the development loop of manually loading your unpacked extension and having to monitor the browser console (Cmd-Shift-J) for logs seems quite slow?&lt;/p&gt;
&lt;p&gt;Maybe I just haven&amp;#39;t been at it long enough but I really hope I find a nicer way to do this.&lt;/p&gt;
</content:encoded></item><item><title>Prefetching considered harmful</title><link>https://bensilverman.co.uk/posts/browser-prefetching/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/browser-prefetching/</guid><pubDate>Mon, 17 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&amp;#39;ve been trying to get &lt;a href=&quot;https://reactrouter.com/api/components/Link#prefetch&quot;&gt;prefetching&lt;/a&gt; working in an app I&amp;#39;m building at the moment, but Firefox has been consistently ignoring my wishes and only fetching on click.&lt;/p&gt;
&lt;p&gt;I looked in my &lt;code&gt;about:config&lt;/code&gt; and, lo and behold, prefetching behaviour had been disabled. I didn&amp;#39;t remember doing that? I turned it back on but it still didn&amp;#39;t work. I checked &lt;code&gt;about:config&lt;/code&gt; again, and it was disabled &lt;em&gt;again&lt;/em&gt;. What??&lt;/p&gt;
&lt;p&gt;To keep it short, &lt;a href=&quot;https://ublockorigin.com/&quot;&gt;uBlock Origin&lt;/a&gt; and &lt;a href=&quot;https://addons.mozilla.org/en-GB/firefox/addon/decentraleyes/&quot;&gt;Decentraleyes&lt;/a&gt;—I&amp;#39;m still not sure if I need both—each had a setting in them to disable prefetching behaviour. Apparently it&amp;#39;s harmful, privacy-wise, because it allows the browser to connect to resources that would&amp;#39;ve otherwise been blocked.&lt;/p&gt;
&lt;p&gt;What now, then? I probably want to keep prefetching disabled because I think I value my privacy that much, which means there&amp;#39;s no point me implementing it. I&amp;#39;m going to look elsewhere for performance gains.&lt;/p&gt;
</content:encoded></item><item><title>An ExpressJS Quickstart</title><link>https://bensilverman.co.uk/posts/expressjs-quickstart/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/expressjs-quickstart/</guid><pubDate>Thu, 13 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this post we&amp;#39;re going to cover how to create your own local web server using the &lt;code&gt;express&lt;/code&gt; &lt;a href=&quot;https://npmjs.com/express&quot;&gt;package&lt;/a&gt;. It&amp;#39;s a pretty straightforward process, so let&amp;#39;s begin!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is a crosspost of an article I wrote over on the Yordevs &lt;a href=&quot;https://yordevs.com&quot;&gt;website&lt;/a&gt; a few weeks ago.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Setting up our project&lt;/h2&gt;
&lt;p&gt;If you haven&amp;#39;t already, set up a new Node project. From there we just need to install &lt;code&gt;express&lt;/code&gt; with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install express
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will download everything that we need to get a basic web server up and running!&lt;/p&gt;
&lt;h2&gt;The code&lt;/h2&gt;
&lt;p&gt;If you&amp;#39;re just looking for some code to copy and paste then the following snippet will be enough.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// index.js
const express = require(&amp;quot;express&amp;quot;);
const app = express();

app.get(&amp;quot;/&amp;quot;, (request, response) =&amp;gt; {
	res.send(&amp;quot;Hello, world!&amp;quot;);
});

app.listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the server just type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can test it working by visiting &lt;code&gt;localhost:3000&lt;/code&gt; in your web browser, and should see something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/wcSGknH.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;A Deeper Explanation&lt;/h2&gt;
&lt;p&gt;If you fancy going into a little more detail, in this section we&amp;#39;ll split up the code above into a few parts and explain what they each do. As a quick reminder, here&amp;#39;s the snippet again (but with comments too!):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 1. Import and setup
const express = require(&amp;quot;express&amp;quot;);
const app = express();

// 2. Setting up endpoints
app.get(&amp;quot;/&amp;quot;, (request, response) =&amp;gt; {
	response.send(&amp;quot;Hello, world!&amp;quot;);
});

// 3. Listening for requests
app.listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. Import and setup&lt;/h3&gt;
&lt;p&gt;In this section we&amp;#39;re doing two things. First, we&amp;#39;re importing our newly installed &lt;code&gt;express&lt;/code&gt; package with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const express = require(&amp;quot;express&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we create an &lt;em&gt;instance&lt;/em&gt; of the Express server by typing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const app = express();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creating instances is helpful in letting us create multiple servers with different &amp;quot;names&amp;quot; that can be referenced for different things. For example you could create three different servers and have them set up to do different things, all without interfering:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const server1 = express();
const server2 = express();
const server3 = express();

// Each server can be set up to listen on a different port (explained later on) and won&amp;#39;t interfere with each other.
server1.listen(3000);
server2.listen(4000);
server3.listen(5000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Setting up endpoints&lt;/h3&gt;
&lt;p&gt;A server&amp;#39;s &lt;em&gt;endpoints&lt;/em&gt; are basically URLs that can be used to access different kinds of data. In the example above with have an endpoint like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;app.get(&amp;quot;/&amp;quot;, (request, response) =&amp;gt; {
	response.send(&amp;quot;Hello, world!&amp;quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates an endpoint for our &lt;code&gt;app&lt;/code&gt; server which responds to GET requests sent to the URL &lt;code&gt;localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you wanted to add another endpoint which did something when you sent a request to &lt;code&gt;localhost:3000/ping&lt;/code&gt;, it would look something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;app.get(&amp;quot;/ping&amp;quot;, (request, response) =&amp;gt; {
	response.send(&amp;quot;Pong!&amp;quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Try this part out in your code, if you want! Consider it a small challenge or something.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The final part is a &lt;a href=&quot;https://www.javascripttutorial.net/javascript-callback/&quot;&gt;callback&lt;/a&gt; which takes the &lt;code&gt;request&lt;/code&gt; and &lt;code&gt;response&lt;/code&gt; as parameters, allowing you to process the request to the server (e.g. &lt;a href=&quot;https://masteringjs.io/tutorials/express/params&quot;&gt;parsing parameters&lt;/a&gt; included in the URL) and your response before you send it back.&lt;/p&gt;
&lt;h3&gt;3. Listening for requests&lt;/h3&gt;
&lt;p&gt;So far we&amp;#39;ve got a server which is based on the ExpressJS package we installed, and a few endpoints which are set up to do some stuff, but how do we actually get the server responding to our requests?&lt;/p&gt;
&lt;p&gt;Much like a real life conversation, we need the server to be &lt;em&gt;listening&lt;/em&gt; for any requests that are sent to it.&lt;/p&gt;
&lt;p&gt;This is, thankfully, a &lt;em&gt;really&lt;/em&gt; easy thing to do.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;app.listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we&amp;#39;re literally telling the server, &lt;em&gt;&amp;quot;listen out for any requests sent to port 3000&amp;quot;&lt;/em&gt; .&lt;/p&gt;
&lt;p&gt;For reference, a port is like a way of dividing a network connection into lots of little numbered &amp;quot;doors&amp;quot; which can then have different types of network traffic assigned to them (e.g. all requests going to our server are on &lt;strong&gt;port 3000&lt;/strong&gt;, all HTTP requests use &lt;strong&gt;port 80&lt;/strong&gt;).&lt;/p&gt;
&lt;p&gt;But yeah, that&amp;#39;s what listening for requests is!&lt;/p&gt;
&lt;p&gt;With all that said and done, you should have your very own web server up and running on your computer. Congratulations!&lt;/p&gt;
</content:encoded></item><item><title>Do your tests fail?</title><link>https://bensilverman.co.uk/posts/fail-tests/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/fail-tests/</guid><pubDate>Tue, 17 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Posting this mainly as a reminder to myself.&lt;/p&gt;
&lt;p&gt;It&amp;#39;s probably not enough to write a unit test, check that it passes, and continue on with your day. You should also check that it can fail too. Make it expect something that&amp;#39;s slightly different from what you &lt;em&gt;really&lt;/em&gt; want and see if it blows up in your face.&lt;/p&gt;
&lt;p&gt;Otherwise, you might&amp;#39;ve just written the unit test equivalent of a &lt;a href=&quot;https://www.merriam-webster.com/dictionary/yes-man&quot;&gt;yes-man&lt;/a&gt; and it won&amp;#39;t really provide any value for you or your meticulously-crafted test suite.&lt;/p&gt;
</content:encoded></item><item><title>Styling Astro with Radix Colors</title><link>https://bensilverman.co.uk/posts/radix-colors-astro/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/radix-colors-astro/</guid><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&amp;#39;m rewriting my website again—this time with Astro—and gave myself the challenge to do it all with vanilla CSS instead of Tailwind, because I was getting a little too dependent.&lt;/p&gt;
&lt;p&gt;One of the things I instantly missed was having access to a collection of high-quality colour palettes right at my fingertips. Luckily, I&amp;#39;m a big fan of &lt;a href=&quot;https://www.radix-ui.com&quot;&gt;Radix&lt;/a&gt; and remembered they released their &lt;a href=&quot;https://www.radix-ui.com/colors&quot;&gt;&amp;quot;colors&amp;quot;&lt;/a&gt; package to provide something very similar.&lt;/p&gt;
&lt;p&gt;There are a couple of ways to use these colours with Astro so I thought I&amp;#39;d make a note of them.&lt;/p&gt;
&lt;h2&gt;Option 1: Importing with CSS&lt;/h2&gt;
&lt;p&gt;I think the simplest way is to write a CSS file and import the colours you want directly in there. They&amp;#39;re all then accessible through the use of CSS variables.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import &amp;quot;@radix-ui/colors/slate.css&amp;quot;;
@import &amp;quot;@radix-ui/colors/cyan.css&amp;quot;;

a {
  color: var(--cyan-9);
	&amp;amp;:hover {
    color: var(--cyan-10);
	}
}
/* Any other styling you want... */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then import this css file anywhere you want to be able to use the colours, like you would any other CSS file in Astro.&lt;/p&gt;
&lt;p&gt;For example, in this &lt;code&gt;Layout&lt;/code&gt; component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;---
const { title } = Astro.props;

import &amp;quot;../styles/colours.css&amp;quot;;
---

&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
	&amp;lt;head&amp;gt;
        &amp;lt;title&amp;gt;Custom Colours&amp;lt;/title&amp;gt;
	&amp;lt;/head&amp;gt;
	&amp;lt;body&amp;gt;
		&amp;lt;article&amp;gt;
			&amp;lt;h1&amp;gt;Custom Colours&amp;lt;/h1&amp;gt;
			&amp;lt;slot /&amp;gt;
		&amp;lt;/article&amp;gt;
	&amp;lt;/body&amp;gt;
	&amp;lt;style&amp;gt;
		h1 {
			color: var(--slate-12);
		}
	&amp;lt;/style&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another bonus here is you can use these same CSS variables in other components, as long as they would be wrapped by this &lt;code&gt;Layout&lt;/code&gt; component at some point in the tree. This is a pretty good option, for example, if you use a &lt;code&gt;BaseLayout&lt;/code&gt; component which you wrap every page in.&lt;/p&gt;
&lt;h2&gt;Option 2: &lt;code&gt;define:vars&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The second option is to import the colour palette via the Component Script and inject them into a style tag with &lt;code&gt;define:vars&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;---
const { title } = Astro.props;

import { slate } from &amp;quot;@radix-ui/colors&amp;quot;;
---

&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
	&amp;lt;head&amp;gt;
		&amp;lt;title&amp;gt;Custom Colours&amp;lt;/title&amp;gt;
	&amp;lt;/head&amp;gt;
	&amp;lt;body&amp;gt;
		&amp;lt;article&amp;gt;
			&amp;lt;h1&amp;gt;Custom Colours&amp;lt;/h1&amp;gt;
			&amp;lt;slot /&amp;gt;
		&amp;lt;/article&amp;gt;
	&amp;lt;/body&amp;gt;
	&amp;lt;style define:vars={slate}&amp;gt;
		h1 {
			color: var(--slate12);
		}
	&amp;lt;/style&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Note!&lt;/strong&gt;
Using the colours with &lt;code&gt;define:vars&lt;/code&gt; gives it a slightly different syntax to before. See how it&amp;#39;s &lt;code&gt;--slate12&lt;/code&gt; instead of &lt;code&gt;--slate-12&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Combining the two&lt;/h2&gt;
&lt;p&gt;There might be a case where you want to globally define &lt;em&gt;some&lt;/em&gt; colours but scope others to a specific component. This can be done by combining both of the methods above!&lt;/p&gt;
&lt;p&gt;I&amp;#39;ve whipped up a quick example of what I mean here: &lt;a href=&quot;https://stackblitz.com/edit/radix-colors-astro?file=src%2Fpages%2Findex.astro&quot;&gt;Stackblitz&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Rewriting my website</title><link>https://bensilverman.co.uk/posts/rewriting-website/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/rewriting-website/</guid><pubDate>Sat, 18 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Just a quick note&lt;/strong&gt; that, since writing this post, I rewrote my website &lt;em&gt;again&lt;/em&gt;. I guess some things are never constant. I&amp;#39;m even thinking about starting it from scratch for a third (fourth?) time because I&amp;#39;m never happy with it and I don&amp;#39;t know what I want.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I recently decided to completely rewrite my website. My old one sat, unchanged, for almost 3 years and I never really put much effort into adding new features to it or updating it with content, despite it being technically possible.&amp;#x20;&lt;/p&gt;
&lt;p&gt;The process just had too much friction and I never felt like had anything big enough to say that it would be worth the effort; I didn&amp;#39;t want to have to open a code editor just to write a little Markdown post, and I &lt;em&gt;certainly&lt;/em&gt; didn&amp;#39;t want to have to start creating pull requests in GitHub to get that content live.&lt;/p&gt;
&lt;p&gt;So, I decided to rewrite it all, and look for a way to write and manage this site&amp;#39;s content that was more inline with a traditional CMS. It would be nice to be able to access it anywhere, from any device, so I&amp;#39;m not restricted only to places that I happen to have cloned my site&amp;#39;s repo to.&lt;/p&gt;
&lt;p&gt;I also fancied a rewrite because I wanted to use &lt;a href=&quot;https://remix.run/&quot;&gt;Remix&lt;/a&gt;. I really like Remix. The loaders and actions and nested routing and &amp;quot;use the web&amp;quot; mantra give you so much control with such a great DX, and I found it really clicked for me when building &lt;a href=&quot;https://lettuce.watch&quot;&gt;other&lt;/a&gt; &lt;a href=&quot;https://insync.rocks&quot;&gt;projects&lt;/a&gt; with it.&lt;/p&gt;
&lt;p&gt;I only stumbled across &lt;a href=&quot;https://keystatic.com/&quot;&gt;Keystatic&lt;/a&gt; very recently but it seemed to be exactly what I was looking for. Following their guide on &lt;a href=&quot;https://keystatic.com/docs/installation-remix&quot;&gt;adding Keystatic to a Remix project&lt;/a&gt;, I was very quickly able to get a working &amp;quot;admin panel&amp;quot; with a really nice editor too. Theoretically I can login to this editor from any device, sign in with GitHub, and then publish posts without ever having to use a terminal or open VSCode. Exactly what I want!&lt;/p&gt;
&lt;p&gt;I&amp;#39;m hoping this massive reduction in friction will lead to me writing more, because definitely something I want to do more, even if it feels quite ropey at first. And I trust Remix to be powerful enough, too, to let me extend this site with whatever I want in the future. If I suddenly decide I need a place to track books that isn&amp;#39;t my &lt;a href=&quot;https://goodreads.com/benslv&quot;&gt;Goodreads&lt;/a&gt;, for example, it&amp;#39;s definitely something I could integrate.&lt;/p&gt;
&lt;p&gt;Thanks for reading! 👋&lt;/p&gt;
</content:encoded></item><item><title>I don&apos;t need a second brain...do I?</title><link>https://bensilverman.co.uk/posts/second-brain/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/second-brain/</guid><pubDate>Tue, 06 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Just watched &lt;a href=&quot;https://maggieappleton.com/&quot;&gt;Maggie Appleton&lt;/a&gt; demo how she uses &lt;a href=&quot;https://tana.inc/&quot;&gt;Tana&lt;/a&gt; to organise all her stuff and felt a weird mix of awe and FOMO? I don&amp;#39;t collect anywhere &lt;em&gt;near&lt;/em&gt; enough data in my day-to-day life to need much of a system to organise it but I kinda wish I did? I love the idea of a Second Brain, all connected together to help me work better and organise my thoughts and interests. On the other hand, maybe it&amp;#39;s just got a nice UI that I like and I&amp;#39;m trying to trick myself into &lt;em&gt;feeling&lt;/em&gt; productive by setting up some kind of new system.&lt;/p&gt;
</content:encoded></item><item><title>Using next-themes with Stitches and Next.js</title><link>https://bensilverman.co.uk/posts/stitches-next-themes/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/stitches-next-themes/</guid><pubDate>Sat, 18 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&amp;#39;ve been building my website with the help of an amazing CSS-in-JSS solution called &lt;a href=&quot;https://stitches.dev&quot;&gt;Stitches&lt;/a&gt;. If you haven&amp;#39;t heard of it before I would definitely recommend taking a peek at their website and giving it a try because I almost instantly converted across from &lt;code&gt;styled-components&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This isn&amp;#39;t a post all about how great I think Stitches is, as much as the dev team would probably love it, though. Instead, I wanted to quickly write-up how I got dark mode working on this site using Next.js, Stitches and a little package called &lt;a href=&quot;https://www.npmjs.com/package/next-themes&quot;&gt;&lt;code&gt;next-themes&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Setup the next-themes provider&lt;/h2&gt;
&lt;p&gt;Install &lt;code&gt;next-themes&lt;/code&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install next-themes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and add its provider to our Next.js &lt;code&gt;App&lt;/code&gt; component.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// _app.tsx

import React from &amp;quot;react&amp;quot;;
import type { AppProps } from &amp;quot;next/app&amp;quot;;
import { ThemeProvider } from &amp;quot;next-themes&amp;quot;;

function App({ Component, pageProps }: AppProps): JSX.Element {
	return (
		&amp;lt;ThemeProvider&amp;gt;
			&amp;lt;Component {...pageProps} /&amp;gt;
		&amp;lt;/ThemeProvider&amp;gt;
	);
}
export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create a dark theme in Stitches&lt;/h2&gt;
&lt;p&gt;Head on over to your Stitches config file (&lt;code&gt;stitches.config.ts&lt;/code&gt;, for me) and import &lt;code&gt;createTheme&lt;/code&gt; from &lt;code&gt;stitches/react&lt;/code&gt;. Then create a new theme for your dark theme like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// stitches.config.ts

import { createStitches, createTheme } from &amp;quot;@stitches/react&amp;quot;;

export const { styled, css, theme } = createStitches({
	// other styling here...
});

// Create your new dark theme below, like this.
export const darkTheme = createTheme(&amp;quot;dark&amp;quot;, {
	colors: {
		text: &amp;quot;black&amp;quot;,
		heading: &amp;quot;grey&amp;quot;,
		background: &amp;quot;white&amp;quot;,
		accent: &amp;quot;cyan&amp;quot;,
	},
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Great! Now we want to set this theme up as an option in &lt;code&gt;next-themes&lt;/code&gt;. Head back over to the &lt;code&gt;_app&lt;/code&gt; file and set it up like so.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// _app.tsx

import React from &amp;quot;react&amp;quot;;
import type { AppProps } from &amp;quot;next/app&amp;quot;;
import { ThemeProvider } from &amp;quot;next-themes&amp;quot;;

// Import your dark theme into the component.
import { darkTheme } from &amp;quot;../stitches.config&amp;quot;;

function App({ Component, pageProps }: AppProps): JSX.Element {
	return (
		&amp;lt;ThemeProvider
			attribute=&amp;quot;class&amp;quot;
			value={{
				dark: darkTheme.toString(), // this .toString() is important
				light: &amp;quot;light&amp;quot;,
			}}
		&amp;gt;
			&amp;lt;Component {...pageProps} /&amp;gt;
		&amp;lt;/ThemeProvider&amp;gt;
	);
}
export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Theme Toggle Component&lt;/h2&gt;
&lt;p&gt;Lastly we just need to make a little button or something to let you toggle between the two colour themes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// ThemeToggle.tsx

import React, { useState, useEffect } from &amp;quot;react&amp;quot;;
import { useTheme } from &amp;quot;next-themes&amp;quot;;

export const ThemeToggle = () =&amp;gt; {
	const [isMounted, setIsMounted] = useState(false);
	const { setTheme, resolvedTheme } = useTheme();

	useEffect(() =&amp;gt; setIsMounted(true), []);

	const toggleTheme = () =&amp;gt; {
		const targetTheme = resolvedTheme === &amp;quot;light&amp;quot; ? &amp;quot;dark&amp;quot; : &amp;quot;light&amp;quot;;

		setTheme(targetTheme);
	};

	// Don&amp;#39;t render the theme-toggling UI before the mounted.
	// See: https://www.npmjs.com/package/next-themes#avoid-hydration-mismatch
	if (!isMounted) {
		return &amp;lt;button&amp;gt;Loading...&amp;lt;/button&amp;gt;;
	}

	return (
		&amp;lt;button onClick={toggleTheme}&amp;gt;
			Switch to {resolvedTheme === &amp;quot;light&amp;quot; ? &amp;quot;dark&amp;quot; : &amp;quot;light&amp;quot;}
		&amp;lt;/button&amp;gt;
	);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the button wherever you want it and you should see your theme switch between dark and light mode. It&amp;#39;ll also remember which mode you left selected and use it on subsequent page refreshes!&lt;/p&gt;
</content:encoded></item><item><title>Creating a reusable media query function</title><link>https://bensilverman.co.uk/posts/styled-components-media-query/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/styled-components-media-query/</guid><pubDate>Thu, 03 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;In short&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// mixins.js

const breakpoints = {
	sm: &amp;quot;640px&amp;quot;,
	md: &amp;quot;768px&amp;quot;,
	lg: &amp;quot;1280px&amp;quot;,
};

export const media = (size = &amp;quot;sm&amp;quot;) =&amp;gt;
	`@media screen and (min-width: ${breakpoints[size]})`;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Explanation&lt;/h2&gt;
&lt;p&gt;So it turns out &lt;a href=&quot;https://stackoverflow.com/a/40723269/12944841&quot;&gt;you can&amp;#39;t use CSS variables with media queries&lt;/a&gt;, which is a bit of a bummer.&lt;/p&gt;
&lt;p&gt;The main reason you might want to do something like this is to create a bunch of reusable media query mixins for yourself, keeping all your breakpoints the same and letting you change them at a moment&amp;#39;s notice. It also saves a few keystrokes and keeps you from having to remember what the exact syntax for the query should be (&amp;quot;is it &lt;code&gt;min-width&lt;/code&gt; or &lt;code&gt;max-width&lt;/code&gt;? Hmm...&amp;quot;)&lt;/p&gt;
&lt;p&gt;Since the function returns a template literal, it can be inserted whatever CSS-in-JS solution you&amp;#39;re using!&lt;/p&gt;
&lt;p&gt;The code above is basically what you&amp;#39;ll need to write. Feel free to extend it with more breakpoints if you like, or add some checks to ensure you can&amp;#39;t enter any incorrect values for the parameter!&lt;/p&gt;
&lt;h3&gt;Usage&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// text.js

import styled from &amp;quot;styled-components&amp;quot;;

import { media } from &amp;quot;./mixins.js&amp;quot;;

const Text = styled.p`
	color: red;

	${media(&amp;quot;sm&amp;quot;)} {
		color: green;
	}

	${media(&amp;quot;md&amp;quot;)} {
		color: orange;
	}

	${media(&amp;quot;md&amp;quot;)} {
		color: blue;
	}
`;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Hello submodules!</title><link>https://bensilverman.co.uk/posts/submodules/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/submodules/</guid><pubDate>Wed, 30 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&amp;#39;ve put all my website content into its own private repo and am loading it as a Git submodule now, thanks to help from some &lt;a href=&quot;https://www.taniarascia.com/git-submodules-private-content/&quot;&gt;helpful&lt;/a&gt; &lt;a href=&quot;https://dbushell.com/2024/07/19/github-private-submodules/&quot;&gt;guides&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;The plan is to start using Obsidian to create and edit content, then push it to GitHub with the &lt;a href=&quot;https://github.com/Vinzent03/obsidian-git&quot;&gt;Obsidian Git&lt;/a&gt; plugin and hopefully everything just updates. I&amp;#39;m not quite sure whether changes to the submodule will trigger the &amp;quot;parent&amp;quot; repo&amp;#39;s GitHub Action to redeploy the site, which is partly why I&amp;#39;m writing this note; I want to check what happens and whether I need to manually redeploy something. If so, I need to find a way to have this trigger ✨ automagically ✨.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Edit:&lt;/strong&gt; The repo seems tied to a specific version of the submodule, and didn&amp;#39;t automatically pull the latest when I made changes. I think the easiest thing would be to setup some kind of cron job in GitHub Actions that runs the &lt;code&gt;git submodule update&lt;/code&gt; command every 30 minutes or so (it could be longer; I don&amp;#39;t publish very frequently) and redeploys if there are any changes. Hopefully that doesn&amp;#39;t eat through my free tier usage limits.&lt;/p&gt;
</content:encoded></item><item><title>How to make Vercel play nice with Remix and Keystatic</title><link>https://bensilverman.co.uk/posts/vercel-remix-keystatic/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/vercel-remix-keystatic/</guid><pubDate>Thu, 23 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;If you just want the code:&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export async function loader() {
	const reader = createReader(process.cwd(), keystaticConfig);

	// This is the important line!
	console.log(
		fs.readdirSync(path.join(process.cwd(), &amp;quot;app&amp;quot;, &amp;quot;content/posts&amp;quot;))
	);

	const posts = await reader.collections.posts.all();

	return json({ posts });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I recently posted about how I decided to &lt;a href=&quot;/posts/rewriting-my-website&quot;&gt;rewrite my website&lt;/a&gt;, choosing to use Keystatic for managing the trillions and trillions of posts I&amp;#39;ve made.&amp;#x20;&lt;/p&gt;
&lt;p&gt;The process was very simple: I set it up in local development, told it to use GitHub for storage, and added some code in my loaders to read said storage and serve the posts. It&amp;#39;s super simple and it works super well...locally. The only tiny issue I ran into was when deploying this same code to production, on a server hosted by Vercel, where absolutely &lt;em&gt;none&lt;/em&gt; of the posts were showing up.&lt;/p&gt;
&lt;p&gt;Anyway I did some poking around and—through sheer, dumb luck—decided to &lt;code&gt;console.log&lt;/code&gt; out the folder containing my posts, to check the output in Vercel&amp;#39;s runtime logs...and suddenly everything showed up!&lt;/p&gt;
&lt;p&gt;I&amp;#39;m not sure if they&amp;#39;re doing something strange where they don&amp;#39;t make files accessible until something &amp;quot;needs&amp;quot; them but I&amp;#39;m very glad I randomly found it and don&amp;#39;t have to switch to another hosting provider or CMS system.&lt;/p&gt;
</content:encoded></item><item><title>Vertical Tabs</title><link>https://bensilverman.co.uk/posts/vertical-tabs/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/vertical-tabs/</guid><pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;So it turns out &lt;a href=&quot;https://www.firefox.com/en-US/features/vertical-tabs/&quot;&gt;Firefox&lt;/a&gt; and &lt;a href=&quot;https://blog.google/products-and-platforms/products/chrome/new-chrome-productivity-features/&quot;&gt;Chrome&lt;/a&gt; both support vertical tabs now.&lt;/p&gt;
&lt;p&gt;I first tried using vertical tabs back when Arc Browser was popular—and hadn&amp;#39;t yet experienced the &lt;a href=&quot;https://eva.ac/blog/arc/&quot;&gt;massive vulnerability&lt;/a&gt; that probably(?) contributed to its &lt;a href=&quot;https://9to5mac.com/2025/05/27/mac-browser-arc-being-discontinued-in-favor-of-new-dia-app/&quot;&gt;discontinuation&lt;/a&gt; back in 2025—and I really liked them. I was a bit sad to have to go back to horizontal tabs at the top of screen and tried extensions like &lt;a href=&quot;https://addons.mozilla.org/en-GB/firefox/addon/tree-style-tab/&quot;&gt;Tree Style Tabs&lt;/a&gt; and &lt;a href=&quot;https://addons.mozilla.org/en-GB/firefox/addon/sidebery/&quot;&gt;Sideberry&lt;/a&gt; to replicate the effect, but having to rely on an extension and manually styling my &lt;code&gt;userChrome.css&lt;/code&gt; to get Firefox to look how I wanted was a pain, so I relented and went back to horizontal tabs.&lt;/p&gt;
&lt;p&gt;Anyway, I don&amp;#39;t have to do that anymore. I&amp;#39;m back to vertical tabs on both my work and my personal computers. I think both browsers have implemented it quite well, although I&amp;#39;d definitely love to see native tree-style tabs for even more organisation. The best part is being able to collapse the tabs down to just their favicon, which gives me &lt;em&gt;soo&lt;/em&gt; much more screen space.&lt;/p&gt;
&lt;p&gt;But the point of me writing this anyway was to say that I&amp;#39;ve noticed I now keep more tabs open than I did when I was using horizontal tabs. Maybe it&amp;#39;s because it&amp;#39;s easier to scroll through a verical list than a horizontal one? I&amp;#39;m able to see the titles of more tabs than before, so perhaps that plays a factor in how much &amp;quot;context&amp;quot; I&amp;#39;m happy to have open at any one time.&lt;/p&gt;
</content:encoded></item><item><title>Texting myself the weather every day</title><link>https://bensilverman.co.uk/posts/weather-sms/</link><guid isPermaLink="true">https://bensilverman.co.uk/posts/weather-sms/</guid><pubDate>Sun, 01 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Opening the Weather app every morning is too much effort, but you know what isn&amp;#39;t? Writing a recurring job to query an API every morning and send you the results in an SMS, delivered right to your &lt;del&gt;doorstep&lt;/del&gt; phone! Come with me as we embark on my short, little journey through this very process.&lt;/p&gt;
&lt;h1&gt;Version 1: A Zapier &amp;quot;Zap&amp;quot;&lt;/h1&gt;
&lt;p&gt;Version 1 of this service was simply just a &amp;quot;zap&amp;quot; on &lt;a href=&quot;https://zapier.com/app/home&quot;&gt;Zapier&lt;/a&gt;. Very easy to connect together a trigger and an action, and start receiving a text message summary every morning at &amp;quot;around 7am&amp;quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./attachments/22138.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./attachments/57033.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Pretty good already!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I really like the summary that Zapier generates, as it has a bit more detail that you might get from a single &lt;a href=&quot;https://www.nodc.noaa.gov/archive/arc0021/0002199/1.1/data/0-data/HTML/WMO-CODE/WMO4677.HTM&quot;&gt;WMO Weather Code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However I still wanted to actually code this myself, for a couple of reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In this very simple version, there&amp;#39;s no way to customise what time the message arrives; It&amp;#39;s locked at an &amp;quot;around 7am&amp;quot; window. My girlfriend also started using this Zap and her first question to me was whether it could arrive before 7am because she&amp;#39;s often leaving for work earlier than that.&lt;/li&gt;
&lt;li&gt;This is something I should be able to code and host myself, without relying on a third-party. Maybe just a matter of principle but I felt like, &amp;quot;if I &lt;em&gt;can&lt;/em&gt; do this myself, I should.&amp;quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Version 2: TypeScript + Twilio + GitHub Actions&lt;/h1&gt;
&lt;p&gt;This was really the sort of thing I wanted to do from the start. Complicated-ish, but with a clear progression and enough moving parts to keep me interested for an afternoon.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript + &lt;a href=&quot;https://open-meteo.com/&quot;&gt;Open-Meteo&lt;/a&gt; SDK for fetching the weather data&lt;/li&gt;
&lt;li&gt;Twilio for sending the SMS (I had to buy another phone number for this!)&lt;/li&gt;
&lt;li&gt;GitHub Actions for setting up a cron job and trigger the send at ~7am every morning&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;OpenMeteo&lt;/h2&gt;
&lt;p&gt;The Open-Meteo service is pretty cool, as it&amp;#39;s a free API that combines weather models from multiple different national services (e.g. MetOffice) to provide you with &amp;quot;the most reliable and accurate weather predictions available&amp;quot;. The use &lt;a href=&quot;https://github.com/google/flatbuffers&quot;&gt;flatbuffers&lt;/a&gt; instead of JSON for serialising their API payloads, which is apparently &amp;quot;more efficient&amp;quot; (not quite sure how, but maybe I&amp;#39;ll update this once I&amp;#39;ve found it). It leads to some funky syntax when accessing the data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const hourlyWeatherData = {
	temperature2m: hourly.variables(0)!.valuesArray(),
	precipitationProbability: hourly.variables(1).valuesArray(),
	weatherCode: hourly.variables(2)!.valuesArray(),
	uvIndex: hourly.variables(3)!.valuesArray(),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The SDK provides a way to fetch a single day&amp;#39;s-worth of forecast by passing &lt;code&gt;forecast_days: 1&lt;/code&gt; in the request params, with it returning the max/min value for each field across the course of that day. I actually ended up foregoing this and specifying the exact date-time range instead, though, because it turns out the &amp;quot;daily forecast&amp;quot; was a full 24h time range and I was occasionally seeing results from before I woke up affecting what was being sent (e.g. a 99% chance of rain at 5am would be what&amp;#39;s used in the SMS, even if the rest of the day was completely dry). By passing a custom time range (7am to 11pm) and manually grabbing the min/max results myself, I could constraint the weather information to the hours that actually mattered to me.&lt;/p&gt;
&lt;p&gt;One improvement I&amp;#39;d like to make to this version is to find a way to generate more detailed summaries. The Zapier version was able to combine information about precipitation, cloud conditions, and temperature into a short, three-sentence summary, whereas my current implementation just uses a single WMO &lt;a href=&quot;https://www.nodc.noaa.gov/archive/arc0021/0002199/1.1/data/0-data/HTML/WMO-CODE/WMO4677.HTM&quot;&gt;Weather Code&lt;/a&gt;, which isn&amp;#39;t as detailed. I could probably roll my own, similar version by mapping the maximum temperature and probability of rain to a set of possible strings (e.g. &amp;gt;70% becomes &amp;quot;stormy&amp;quot;).&lt;/p&gt;
&lt;h2&gt;Twilio&lt;/h2&gt;
&lt;p&gt;I didn&amp;#39;t do a huge amount of shopping around, here. I know people use Twilio for sending text messages, and I needed to send text messages, so I used Twilio. Great job, Twilio Marketing team! 👍&lt;/p&gt;
&lt;p&gt;Before fully committing, though, I did some back-of-the-envelope maths to estimate how much this sort of system might cost. Twilio charge £0.89022/month for a phone number, and £0.0463 for every SMS you send from that phone number. Based off the minimum &lt;strong&gt;&lt;em&gt;£20&lt;/em&gt;&lt;/strong&gt; top-up charge to use their service, I reckon I could keep this live for maybe...5 months before I need to top-up? Not too bad for a fun personal project.&lt;/p&gt;
&lt;p&gt;The Twilio SDK was actually really easy to set up and use. You basically just pass a string to be used as the message body and tell it which phone number to send from. One slightly annoying thing is there doesn&amp;#39;t seem to be a way to send to multiple numbers at once, so you have to wrap this call in a &lt;code&gt;for&lt;/code&gt; loop if, like me, you&amp;#39;re sending to more than one person.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const client = Twilio(twilioSid, twilioAuthToken);

for (const phoneNumber of phoneNumbers) {
	client.messages.create({
		to: toPhoneNumber,
		from: fromPhoneNumber,
		body: messageBody,
	});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The Twilio SDK was actually really easy to set up and use.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I can&amp;#39;t say the same about their website, though. It&amp;#39;s pretty awful to navigate around and required way too much clicking around to find the billing page and understand where my money&amp;#39;s going.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;GitHub Actions&lt;/h2&gt;
&lt;p&gt;With everything pieced together and working on my machine, I turned to GitHub to provide me with a free, easy way to schedule these text messages and send them out at the same time every day.&lt;/p&gt;
&lt;p&gt;Admittedly, I caved here and used Gemini to help me write the Workflow script, because I feel like this is an area where &lt;a href=&quot;LLMs%20are%20great%20for%20GitHub%20Workflows.md&quot;&gt;LLMs are quite helpful&lt;/a&gt;. In a couple of queries I had the script I wanted—or, at least, enough of a script to be able to tinker with and adjust by myself—and could start testing! It&amp;#39;s a simple script that runs at 6:45am every morning, scheduled with a cron job.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;name: Send SMS at 6:45am every morning
on:
    schedule:
		- cron: &amp;quot;45 5 * * *&amp;quot; # timezones issue (see below) 🙃
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Interestingly, &lt;a href=&quot;https://github.com/orgs/community/discussions/13454&quot;&gt;GitHub Workflows don&amp;#39;t support timezones&lt;/a&gt;! This means that, if I want to sent a text every morning at 7am, when on BST (British Summer Time) I actually need to schedule it for &lt;em&gt;6am&lt;/em&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Et voilà!&lt;/em&gt; My work is done and my phone received a text at (roughly) 6:45 every morning!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachments/72120.png&quot; alt=&quot;attachments/72120.png&quot;&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>