Shellsharks Blogroll - BlogFlock2026-07-08T00:16:54.186ZBlogFlockAdepts of 0xCC, destructured, Trail of Bits Blog, Aaron Parecki, fLaMEd, Westenberg, gynvael.coldwind//vx.log (pl), James' Coffee Blog, joelchrono, Evan Boehs, Kev Quirk, cool-as-heck, Posts feed, Sophie Koonin, cmdr-nova@internet:~$, <span>Songs</span> on the Security of Networks, Werd I/O, Johnny.Decimal, Robb Knight, Molly White, Hey, it's Jason!, Terence Eden’s BlogRE: On Form Factor - Joel's Log Fileshttps://joelchrono.xyz/blog/on-form-factor2026-07-07T21:25:00.000Z<p>Josh, a fellow member of the TWG server, wrote an awesome piece about <a href="https://espressotonic.beehiiv.com/p/on-form-factor">the form factor</a> of the devices and stuff he carries and how that affects the way he uses them! Since we are celebrating <a href="https://robertbirming.com/julyreply/">July Reply</a>, this post seemed like the perfect way for me to reply to!.</p>
<p>In a way, what the premise boils down to is what properties should an object have in order to be used <em>with intent</em> and not ignored?</p>
<p>The way we can afford to spend time intentionally, playing videogames, reading a book or listening to music, is heavily dependant in the context of our lives.</p>
<p>Right now the most time I get to spend doing such activities is on my <a href="/blog/the-time-commuting-to-work">commutes to work</a>, but going to the gym, arriving early to work, or simply being home, come with their own set of situations.</p>
<h2 id="on-commutes">On commutes</h2>
<p>During my commute, I have plenty of advantages. I have free time, I have nothing else to do, and I can carry a backpack with fun trinkets inside.</p>
<p>This means that I can bring my Nintendo Switch and play it the whole trip without any problem. Or any other device for that matter.</p>
<p>However, not all commutes are the same, bringing the Switch is only worth it if my commutes are long and if I really just go from home to work and viceversa.</p>
<p>Sometimes, my commute takes an hour—perfect for some Switch gaming!—but other times, it only takes about 25 minutes, and that makes things a bit more tight. Those shorter sessions are excellent and they were plenty of time for me to complete something like <a href="/blog/metal-gear-solid-peace-walker/"><em>Metal Gear Solid: Peace Walker</em></a> or <em>Fire Emblem Awakening</em>. However, I was lucky enough to have longer commutes when I played <a href="/blog/metroid-prime-remastered/"><em>Metroid Prime Remastered</em></a>.</p>
<p>Ocassionaly, I will get off the work bus on my way back home, and take the public transport, this happens when I’m heading to the gym, or to pick-up a delivery. In such a situation, I don’t dare to bring out my Nintendo Switch in public—I don’t want it stolen—and I won’t take out my PSP either, because the buses here are wild and if I drop it, the battery and its cover popping out who knows where.</p>
<p>Something like my <a href="/blog/the-gba-experience-i-wanted/">Anbernic RG35XX SP</a> is often ideal, much cheaper and smaller, and while not sturdier than the PSP overall, the battery doesn’t pop out—although the cheap plastic will definitely chip.</p>
<p>Of course, this is assuming I can sit. There are seasons where buses will be full of people all the time, and it’s in those moments where my <em>Miyoo Mini Flip</em> comes to the rescue! I actually haven’t used it that much, but when I know I’ll probably have to stand up among lots of people, using it to play one-handed-friendly games is awesome.</p>
<p>However, in that case, I may prefer to read some pages on my <a href="/blog/early-days-with-xteink-x4/">XTEINK X4</a>. And yes, I am one of those weirdos that can actually read while on the move, even on a bus where I keep swaying in every direction at every stop.</p>
<p>Another reason I wouldn’t carry my Switch is because of the added space taken by it. A huge deterrent for me to go to the gym is carrying my extra clothing. I also don’t feel comfortable leaving such a device alone, even when there are locker rooms.</p>
<h2 id="at-the-gym">At the gym</h2>
<p>I don’t use gaming handhelds at the gym. I probably shouldn’t care of what others may think if I did, but I do. Besides, it’s much easier for me to get distracted from the excercise by it, or slowing down my walking pace because of the game. Even if I feel justified when others spend that time on their phones resting for five whole minutes.</p>
<p>I simply stick to a podcast when at the gym, put my earphones and ignore everything else. My body focuses on the excercise and my mind is entertained by an episode of <em>Wolf 359</em>.</p>
<p>However, every once in a while I’ve been bold enough to take out my Kobo reader and read a whole chapter of <em>The Expanse</em> while using the threadmill. I have done the same thing with my XTEINK without much issues.</p>
<p>This is only really useful while on a walk or stationary bike, as most machinery lacks a decent place for me to place the ereader in a good position for me to read. Plus, getting stuck staring at a page I already completed because of an excercise would probably drive me mad.</p>
<p>Indeed, music and podcasts are best here, though the XTEINK’s form factor is nice to carry without feeling uncomfortable on my pocket for the rest of my routine. If it’s my Kobo, I’d have to bring it on hand, leave it on the floor or some other random surfaces, or store it in the locker after the warm-up.</p>
<h2 id="at-work">At work</h2>
<p>To be clear I <em>do not</em> play with my handheld consoles during work hours.</p>
<p>Fine, fine… maybe a casual session of <em>Slice & Dice</em> on my phone every once in a while.</p>
<p>Still, for the longest time the fact that the Kobo looks like paper allowed me to just have it by my side and read a few pages here and there. The form factor was ideal to pass off as a notebook—or maybe people knew sometimes but nobody cared.</p>
<p>Eventually, when the XTEINK came about, things got a lot easier! Although I still use both interchangeably, as it seems not many people mind about telling me to <em>not</em> read books at work…</p>
<p>Oh, right, outside of work hours, I do play with my handhelds sometimes, whenever my commute is short and I arrive early at my workplace! I only do this with smaller form factors, latest of which is my <a href="/blog/nintendo-3ds/">New Nintendo 3DS XL</a>, that has been the largest of the bunch, but still slim enough for me to use before more people start showing up. The Nintendo Switch is way too big and I don’t need to bring attention to it, honestly.</p>
<h2 id="at-home">At home</h2>
<p>Tthe only thing that stops me from gaming, reading, or doing whatever I want, is the fact that I don’t live by myself!</p>
<p>Now, it’s not that I can’t do something I want, I am simply reminded of my responsibilities and aware of the fact that everyone in the house has to fulfill them for the sake of peace, harmony and love!</p>
<p>Because of this, I mostly stick to devices that allow me to have quick sessions, or some leisure time on the TV with my Nintendo Switch, when every duty of mine is taken care of—or when there’s nobody at home to stop me.</p>
<p>I rarely read at home using my Kobo or my XTEINK, if I’m honest, as I would rather spend my time doing things like playing longer games with a narrative that may end up too disjointed when split into short bursts.</p>
<p>This is also the one moment where I actually don’t mind reading a physical book as well, something different from my sci-fi novels or Manga. It’s where I get to read or study the bible, rather than entertainment.</p>
<p>Podcasts also win a lot of ground especially during chores here, great background noise when doing dishes or mopping the floor.</p>
<p>All in all, the form factor matters less when there’s no real limit to what I can do, I guess.</p>
<h2 id="finishing-thoughts">Finishing thoughts</h2>
<p>Josh’s final paragraphs focused on the intentionality of choosing a specific device for a specific moment, for a specific use. Every gadget having a certain meaning, a form factor that makes it ideal for a time, a place, or a state of mind.</p>
<p>Especially so when so much out there is trying to convinced of how encompassing everything into a single point—the smartphone—is the ideal. In the end, it’s all too overwhelming, and overdependence on it only ends up hurting us, being a distraction to what matters in the moment.</p>
<p>That was some pretty good stuff, and I feel I sort of wandered off into what use every device of mine has in many situations. There’s a couple of other things I could have talked about, such as going out shopping or similar, but it’s clear that the tiniest devices are often great when they are meant to fill up empty spaces rather than being the focus.</p>
<p>At last, this is enough for a post, I’d say! I am also going to continue playing some <em>Fire Emblem Awakening</em> on my 3DS after I’m done with this.</p>
<p>This is day 92 of <a href="https://100daystooffload.com">#100DaysToOffload</a>.</p>
<p>
<a href="mailto:me@joelchrono.xyz?subject=RE: On Form Factor">Reply to this post via email</a> |
<a href="https://fosstodon.org/@joel/116880830558317275">Reply on Fediverse</a>
</p>Implementing standard.site on a nonstandard site - Posts feedhttps://www.coryd.dev/posts/2026/implementing-standardsite-on-a-nonstandard-site2026-07-07T21:13:00.000Z<div class="e-content"><p>Now that <a href="https://www.coryd.dev/colophon">this site is written in Go</a><sup id="fnref:1" class="footnote-ref">1</sup>, I've turned to restoring and adding more features connecting it to the open web. While I maintain a healthy skepticism of Bluesky the company, I'm enamored with open protocols and <a href="https://en.wikipedia.org/wiki/AT_Protocol">ATProto</a> falls under that umbrella.</p>
<p>ATProto exists apart from Bluesky and Bluesky is built on top of ATProto.</p>
<p><a href="https://en.wikipedia.org/wiki/AT_Protocol">Wikipedia</a></p>
<blockquote>
<p>The AT Protocol (Authenticated Transfer Protocol, pronounced "at protocol", commonly shortened to ATproto or "ATP") is a protocol and set of open standards for decentralized publishing and distribution of self-authenticating data within the social web. It serves as the technical foundation of the Bluesky social network, originally developed as a reference implementation for the protocol, as well as an ecosystem of interoperable social applications and services collectively referred to as the ATmosphere.</p>
</blockquote>
<p>After reluctantly joining <a href="https://www.coryd.dev/posts/2026/lifting-mastodon-rate-limits">Bluesky</a>, I saw a host of folks implementing <a href="https://standard.site">standard.site</a>.</p>
<blockquote>
<p>Standard.site provides shared lexicons for long-form publishing on AT Protocol. Making content easier to discover, index, and move across the ATmosphere.</p>
</blockquote>
<p>It's another protocol to support, another network, another implementation, another maintenance burden and all that, but it gets you a fancy button and avatar attached to posts. How could I resist?</p>
<p>My implementation is hand-rolled against Bluesky's endpoints since that's where my PDS lives.<sup id="fnref:2" class="footnote-ref">2</sup></p>
<aside>
<p><a href="https://en.wikipedia.org/wiki/AT_Protocol#Personal_Data_Servers">Wikipedia, again:</a> Personal Data Servers (PDSes) host user repositories and their associated media. They also serve as the network access point for users, facilitating repository updates, backups, data queries, and user requests.</p>
</aside>
<article class="entry-card h-entry"><aside class="meta"><time class="dt-published" datetime="2026-07-07T17:21:00Z"><a href="https://www.coryd.dev/links/publishing-on-the-atmosphere-with-standardsite" class="u-url">July 7, 2026</a></time> </aside><h3><a href="https://piccalil.li/blog/publishing-on-the-atmosphere-with-standardsite/?ref=main-rss-feed" title="Publishing on the Atmosphere with Standard.site" class="p-name u-bookmark-of">Publishing on the Atmosphere with Standard.site</a> via <a href="https://vale.rocks">Declan Chidlow</a></h3><blockquote><p>Standard.site provides a set of lexicons for publishing long-form content on the internet using the same protocol used under the hood by Bluesky.</p>
</blockquote><div class="badges"><a href="https://www.coryd.dev/tags/tech" class="badge p-category">tech</a><a href="https://www.coryd.dev/tags/atproto" class="badge p-category">atproto</a><a href="https://www.coryd.dev/tags/bluesky" class="badge p-category">bluesky</a></div></article><h2 id="getting-things-working">Getting things working</h2>
<p>There are two record types that are required as part of this implementation: <code>site.standard.publication</code> which describes the site — name, description, url, theme and icon. <code>site.standard.document</code> is a record required for each piece of content, each pointing back to my site (the publication).</p>
<p>When I post a link the <a href="https://en.wikipedia.org/wiki/AT_Protocol#AppViews">AppView</a> crawls the page looking for two tags: <code><link rel="site.standard.document"></code> and <code><link rel="site.standard.publication"></code>. These tags have <code>href</code> attributes that are resolved against my PDS, verifying ownership by also fetching my <a href="https://www.coryd.dev/.well-known/site.standard.publication">/.well-known/site.standard.publication</a> endpoint.</p>
<p>I use a handful of <a href="https://riverqueue.com">River</a> jobs to make this work. Records are authored as plain <code>map[string]any</code> objects that are marshalled to JSON and <code>PUT</code> via <code>com.atproto.repo.putRecord</code>.</p>
<p>I post a bunch of different content types on this site, many of which are syndicated to Mastodon and Bluesky and each type that is syndicated is now published as part of the standard.site implementation. The shape of syndicated content is typically: title, description, hashtags and a link to the content. Only movies, books and concerts I've written something about are syndicated, while all posts are syndicated. The query to fetch movie data from my <a href="https://www.postgresql.org">PostgreSQL</a> database looks like this:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span> title<span class="token punctuation">,</span> COALESCE<span class="token punctuation">(</span>description<span class="token punctuation">,</span><span class="token string">''</span><span class="token punctuation">),</span> url<span class="token punctuation">,</span> last_watched<span class="token punctuation">,</span> COALESCE<span class="token punctuation">(</span>tags<span class="token punctuation">,</span><span class="token string">'{}'</span><span class="token punctuation">)</span> <span class="token keyword">FROM</span> optimized_movies <span class="token keyword">WHERE</span> review <span class="token keyword">IS</span> <span class="token keyword">NOT</span> <span class="token keyword">NULL</span> <span class="token keyword">AND</span> review <span class="token operator">!=</span> <span class="token string">''</span> <span class="token keyword">AND</span> last_watched <span class="token keyword">IS</span> <span class="token keyword">NOT</span> <span class="token keyword">NULL</span> <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> last_watched <span class="token keyword">DESC</span>
</code></pre>
<p>The constructed document record looks like this:</p>
<pre class="language-go"><code class="language-go">record <span class="token operator">:=</span> <span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">any</span><span class="token punctuation">{</span>
<span class="token string">"$type"</span><span class="token punctuation">:</span> <span class="token string">"site.standard.document"</span><span class="token punctuation">,</span>
<span class="token string">"site"</span><span class="token punctuation">:</span> publicationURI<span class="token punctuation">,</span> <span class="token comment">// AT-URI back to the publication</span>
<span class="token string">"title"</span><span class="token punctuation">:</span> item<span class="token punctuation">.</span>Title<span class="token punctuation">,</span>
<span class="token string">"publishedAt"</span><span class="token punctuation">:</span> item<span class="token punctuation">.</span>Date<span class="token punctuation">.</span><span class="token function">UTC</span><span class="token punctuation">().</span><span class="token function">Format</span><span class="token punctuation">(</span>time<span class="token punctuation">.</span>RFC3339<span class="token punctuation">),</span>
<span class="token string">"path"</span><span class="token punctuation">:</span> item<span class="token punctuation">.</span>URL<span class="token punctuation">,</span> <span class="token comment">// also the rkey seed</span>
<span class="token punctuation">}</span>
</code></pre>
<p>The publication record looks like this:</p>
<pre class="language-go"><code class="language-go"><span class="token operator">...</span>
record <span class="token operator">:=</span> <span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">any</span><span class="token punctuation">{</span>
<span class="token string">"$type"</span><span class="token punctuation">:</span> <span class="token string">"site.standard.publication"</span><span class="token punctuation">,</span>
<span class="token string">"name"</span><span class="token punctuation">:</span> name<span class="token punctuation">,</span>
<span class="token string">"url"</span><span class="token punctuation">:</span> siteURL<span class="token punctuation">,</span>
<span class="token string">"preferences"</span><span class="token punctuation">:</span> <span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">any</span><span class="token punctuation">{</span>
<span class="token string">"showInDiscover"</span><span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
<span class="token punctuation">},</span>
<span class="token string">"basicTheme"</span><span class="token punctuation">:</span> <span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">any</span><span class="token punctuation">{</span>
<span class="token string">"$type"</span><span class="token punctuation">:</span> <span class="token string">"site.standard.theme.basic"</span><span class="token punctuation">,</span>
<span class="token string">"background"</span><span class="token punctuation">:</span> <span class="token function">rgbColor</span><span class="token punctuation">(</span><span class="token number">239</span><span class="token punctuation">,</span> <span class="token number">242</span><span class="token punctuation">,</span> <span class="token number">246</span><span class="token punctuation">),</span>
<span class="token string">"foreground"</span><span class="token punctuation">:</span> <span class="token function">rgbColor</span><span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">,</span> <span class="token number">14</span><span class="token punctuation">,</span> <span class="token number">19</span><span class="token punctuation">),</span>
<span class="token string">"accent"</span><span class="token punctuation">:</span> <span class="token function">rgbColor</span><span class="token punctuation">(</span><span class="token number">78</span><span class="token punctuation">,</span> <span class="token number">83</span><span class="token punctuation">,</span> <span class="token number">89</span><span class="token punctuation">),</span>
<span class="token string">"accentForeground"</span><span class="token punctuation">:</span> <span class="token function">rgbColor</span><span class="token punctuation">(</span><span class="token number">239</span><span class="token punctuation">,</span> <span class="token number">242</span><span class="token punctuation">,</span> <span class="token number">246</span><span class="token punctuation">),</span>
<span class="token punctuation">},</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> description <span class="token operator">!=</span> <span class="token string">""</span> <span class="token punctuation">{</span>
record<span class="token punctuation">[</span><span class="token string">"description"</span><span class="token punctuation">]</span> <span class="token punctuation">=</span> description
<span class="token punctuation">}</span>
<span class="token keyword">if</span> icon <span class="token operator">!=</span> <span class="token keyword">nil</span> <span class="token punctuation">{</span>
record<span class="token punctuation">[</span><span class="token string">"icon"</span><span class="token punctuation">]</span> <span class="token punctuation">=</span> icon
<span class="token punctuation">}</span>
<span class="token keyword">return</span> record
<span class="token operator">...</span>
</code></pre>
<aside>
<p>The icon referenced here isn't a URL. <code>atproto</code> records reference blobs, so I fetched my existing OG image and uploaded it to the PDS first, then dropped the returned blob ref into the record. I derived the theme colors from the canonical <code>vars.css</code> used to style my site.</p>
</aside>
<p>I created a pair of standard.site specific commands in the CMS for this site. One to sync my site record to my PDS and another to sync my backlog of posts. I ran both, verified the records, tried a test post and nothing happened. I hoped it was a caching or propagation issue, waited and still nothing. I hit a few walls building this but, being unreasonably stubborn, I continued on.</p>
<h3 id="bug-1-apex-vs-www">Bug 1: apex vs. www</h3>
<p>As best I can tell, the <code>AppView</code> verifies my <code>.well-known</code> endpoint using a non-JavaScript call. My username on Bluesky is my apex domain but that redirects to <code>www</code>. I set the site record to <code>www.coryd.dev</code> and allowed the <code>.well-known</code> path to resolve at the apex in my Caddy config.</p>
<h3 id="bug-2-set-a-stable-record-key">Bug 2: set a stable record key</h3>
<p>For this to work, the record key in Bluesky <em>must</em> match the one embedded in the head of each document. However, a number of my content types have a published date that can shift over time — books when I read them again and movies when I watch them again. I store a record for each read and watch but the published date is updated to the most recent each time. I chose to derive the <code>rkey</code> from the URL for each content type alone.</p>
<aside>
<p>Every record on a PDS lives in a collection at a record key. Its address is <code>at://{my-did}/site.standard.document/{rkey}</code>. I get to choose the <code>rkey</code> which, naturally, is how I managed to break things.</p>
</aside>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token function">ATProtoTID</span><span class="token punctuation">(</span>urlPath <span class="token class-name">string</span><span class="token punctuation">)</span> <span class="token class-name">string</span> <span class="token punctuation">{</span>
h <span class="token operator">:=</span> fnv<span class="token punctuation">.</span><span class="token function">New64a</span><span class="token punctuation">()</span>
_<span class="token punctuation">,</span> _ <span class="token punctuation">=</span> h<span class="token punctuation">.</span><span class="token function">Write</span><span class="token punctuation">([]</span><span class="token class-name">byte</span><span class="token punctuation">(</span>urlPath<span class="token punctuation">))</span>
n <span class="token operator">:=</span> h<span class="token punctuation">.</span><span class="token function">Sum64</span><span class="token punctuation">()</span> <span class="token operator">&</span> <span class="token number">0x7FFFFFFFFFFFFFFF</span>
<span class="token comment">// base32-encode to 13 chars</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Each URL is unique, stored with the record and can be used to derive a stable <code>rkey</code>.</p>
<h3 id="bug-3-theme-object-validation">Bug 3: theme object validation</h3>
<p>A simple, but fun one: failing to set <code>$type</code> on the theme object will cause the publication block to be dropped by the Bluesky parser.</p>
<p>I solved this one by comparing the structure of my publication record to Mat Maquis', line by line, after reading his <a href="https://wil.to/posts/implementing-standard-site/">implementation post</a>.</p>
<h2 id="stapling-things-together">Stapling things together</h2>
<p>When I syndicate something to Bluesky, the external embed carries a pair of references pointing at the document and publication:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">if</span> refs <span class="token operator">:=</span> w<span class="token punctuation">.</span><span class="token function">standardSiteRefs</span><span class="token punctuation">(</span>ctx<span class="token punctuation">,</span> session<span class="token punctuation">,</span> content<span class="token punctuation">);</span> <span class="token class-name">len</span><span class="token punctuation">(</span>refs<span class="token punctuation">)</span> <span class="token punctuation">></span> <span class="token number">0</span> <span class="token punctuation">{</span>
external<span class="token punctuation">[</span><span class="token string">"associatedRefs"</span><span class="token punctuation">]</span> <span class="token punctuation">=</span> refs
<span class="token punctuation">}</span>
record<span class="token punctuation">[</span><span class="token string">"embed"</span><span class="token punctuation">]</span> <span class="token punctuation">=</span> <span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">any</span><span class="token punctuation">{</span>
<span class="token string">"$type"</span><span class="token punctuation">:</span> <span class="token string">"app.bsky.embed.external"</span><span class="token punctuation">,</span>
<span class="token string">"external"</span><span class="token punctuation">:</span> external<span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre>
<p>To properly derive these, the syndicated document has to be on the PDS before Bluesky indexes the post:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// Bluesky is delayed so the standard.site document record lands on the PDS</span>
<span class="token comment">// before Bluesky indexes the post</span>
<span class="token punctuation">{</span>jobs<span class="token punctuation">.</span>SyncSingleStandardSiteDocumentArgs<span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">},</span> <span class="token operator">&</span>river<span class="token punctuation">.</span>InsertOpts<span class="token punctuation">{</span>UniqueOpts<span class="token punctuation">:</span> dedupeInFlight<span class="token punctuation">}},</span>
<span class="token punctuation">{</span>jobs<span class="token punctuation">.</span>SyndicateToBlueskyArgs<span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">},</span> <span class="token operator">&</span>river<span class="token punctuation">.</span>InsertOpts<span class="token punctuation">{</span>
ScheduledAt<span class="token punctuation">:</span> time<span class="token punctuation">.</span><span class="token function">Now</span><span class="token punctuation">().</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token number">30</span> <span class="token operator">*</span> time<span class="token punctuation">.</span>Second<span class="token punctuation">),</span> UniqueOpts<span class="token punctuation">:</span> dedupeInFlight<span class="token punctuation">}},</span>
</code></pre>
<aside>
<p>Each ref is a <code>strongRef</code>: a record's <code>AT-URI</code> paired with a hash of its exact contents. This allows the embed to tell Bluesky precisely which document and publication this link maps to, rather than making it recrawl the page.</p>
</aside>
<p>The other fun part of this all is that these failures (inasmuch as Bluesky was concerned) were silent. My implementation passed the available validators I tried and the published records passed the eye test, but they failed Bluesky's validation.</p>
<article class="entry-card h-entry"><div class="status-card-body"><aside class="meta"><time class="dt-published" datetime="2026-06-13T03:24:00Z"><a href="https://www.coryd.dev/status/this-standardsite-2026-06-13-03-24-25" class="u-url">June 13, 2026</a></time> <span aria-hidden="true">•</span> <a href="https://www.coryd.dev/status">Status</a></aside><div class="e-content"><p>This standard.site stuff is super confusing</p>
</div><a class="u-url visually-hidden" href="https://www.coryd.dev/status/this-standardsite-2026-06-13-03-24-25" aria-hidden="true" tabindex="-1">/status/this-standardsite-2026-06-13-03-24-25</a></div></article><h3 id="actually-publishing">Actually publishing</h3>
<p>Every job authenticates when run using an app password against <code>com.atproto.server.createSession</code> which returns a short-lived bearer token (<code>accessJwt</code>) and confirms my <code>DID</code>:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token function">atprotoCreateSession</span><span class="token punctuation">(</span>ctx context<span class="token punctuation">.</span>Context<span class="token punctuation">,</span> identifier<span class="token punctuation">,</span> appPassword <span class="token class-name">string</span><span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token operator">*</span>blueskySession<span class="token punctuation">,</span> <span class="token class-name">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
body<span class="token punctuation">,</span> _ <span class="token operator">:=</span> json<span class="token punctuation">.</span><span class="token function">Marshal</span><span class="token punctuation">(</span><span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">string</span><span class="token punctuation">{</span>
<span class="token string">"identifier"</span><span class="token punctuation">:</span> identifier<span class="token punctuation">,</span>
<span class="token string">"password"</span><span class="token punctuation">:</span> appPassword<span class="token punctuation">,</span>
<span class="token punctuation">})</span>
<span class="token comment">// POST to /xrpc/com.atproto.server.createSession → { accessJwt, did }</span>
<span class="token operator">...</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Once I have the token, I can write the document record:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token function">atprotoPutRecord</span><span class="token punctuation">(</span>ctx context<span class="token punctuation">.</span>Context<span class="token punctuation">,</span> session <span class="token operator">*</span>blueskySession<span class="token punctuation">,</span> collection<span class="token punctuation">,</span> rkey <span class="token class-name">string</span><span class="token punctuation">,</span> record <span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">any</span><span class="token punctuation">)</span> <span class="token class-name">error</span> <span class="token punctuation">{</span>
body<span class="token punctuation">,</span> _ <span class="token operator">:=</span> json<span class="token punctuation">.</span><span class="token function">Marshal</span><span class="token punctuation">(</span><span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">any</span><span class="token punctuation">{</span>
<span class="token string">"repo"</span><span class="token punctuation">:</span> session<span class="token punctuation">.</span>DID<span class="token punctuation">,</span>
<span class="token string">"collection"</span><span class="token punctuation">:</span> collection<span class="token punctuation">,</span>
<span class="token string">"rkey"</span><span class="token punctuation">:</span> rkey<span class="token punctuation">,</span>
<span class="token string">"record"</span><span class="token punctuation">:</span> record<span class="token punctuation">,</span>
<span class="token punctuation">})</span>
<span class="token comment">// POST /xrpc/com.atproto.repo.putRecord with Authorization: Bearer <accessJwt></span>
<span class="token operator">...</span>
<span class="token punctuation">}</span>
</code></pre>
<p><code>putRecord</code> creates the record if the <code>rkey</code> is unique and updates it if not. I can re-run my backlog sync safely knowing that it will update old records and add new ones but not destroy anything.</p>
<p>Syndicating to Bluesky itself is a separate and straightforward process. The record looks like this:</p>
<pre class="language-go"><code class="language-go">record <span class="token operator">:=</span> <span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">any</span><span class="token punctuation">{</span>
<span class="token string">"$type"</span><span class="token punctuation">:</span> <span class="token string">"app.bsky.feed.post"</span><span class="token punctuation">,</span>
<span class="token string">"text"</span><span class="token punctuation">:</span> text<span class="token punctuation">,</span>
<span class="token string">"createdAt"</span><span class="token punctuation">:</span> time<span class="token punctuation">.</span><span class="token function">Now</span><span class="token punctuation">().</span><span class="token function">UTC</span><span class="token punctuation">().</span><span class="token function">Format</span><span class="token punctuation">(</span>time<span class="token punctuation">.</span>RFC3339<span class="token punctuation">),</span>
<span class="token string">"langs"</span><span class="token punctuation">:</span> <span class="token punctuation">[]</span><span class="token class-name">string</span><span class="token punctuation">{</span><span class="token string">"en"</span><span class="token punctuation">},</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Unlike Mastodon, however, you can't simply send text. For tags to be active rather than plain text, you have to attach them as <code>facets</code>:</p>
<pre class="language-go"><code class="language-go">hashRE <span class="token operator">:=</span> regexp<span class="token punctuation">.</span><span class="token function">MustCompile</span><span class="token punctuation">(</span><span class="token string">`#([a-zA-Z][a-zA-Z0-9_]*)`</span><span class="token punctuation">)</span>
<span class="token keyword">for</span> _<span class="token punctuation">,</span> m <span class="token operator">:=</span> <span class="token keyword">range</span> hashRE<span class="token punctuation">.</span><span class="token function">FindAllStringIndex</span><span class="token punctuation">(</span>text<span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
tag <span class="token operator">:=</span> text<span class="token punctuation">[</span>m<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token operator">+</span><span class="token number">1</span> <span class="token punctuation">:</span> m<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]]</span>
facets <span class="token punctuation">=</span> <span class="token class-name">append</span><span class="token punctuation">(</span>facets<span class="token punctuation">,</span> <span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">any</span><span class="token punctuation">{</span>
<span class="token string">"index"</span><span class="token punctuation">:</span> <span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">int</span><span class="token punctuation">{</span><span class="token string">"byteStart"</span><span class="token punctuation">:</span> m<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">],</span> <span class="token string">"byteEnd"</span><span class="token punctuation">:</span> m<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]},</span>
<span class="token string">"features"</span><span class="token punctuation">:</span> <span class="token punctuation">[]</span><span class="token keyword">map</span><span class="token punctuation">[</span><span class="token class-name">string</span><span class="token punctuation">]</span><span class="token class-name">any</span><span class="token punctuation">{{</span><span class="token string">"$type"</span><span class="token punctuation">:</span> <span class="token string">"app.bsky.richtext.facet#tag"</span><span class="token punctuation">,</span> <span class="token string">"tag"</span><span class="token punctuation">:</span> tag<span class="token punctuation">}},</span>
<span class="token punctuation">})</span>
<span class="token punctuation">}</span>
</code></pre>
<p>You have to do the same for URLs (<code>facet#link</code>) and handles (<code>facet#mention</code> — this also requires resolving the handle to a DID, not providing the readable name). It's also worth noting that Bluesky uses <code>createRecord</code> rather than <code>putRecord</code> here which creates its own fresh <code>rkey</code>:</p>
<pre class="language-go"><code class="language-go">req<span class="token punctuation">,</span> _ <span class="token operator">:=</span> http<span class="token punctuation">.</span><span class="token function">NewRequestWithContext</span><span class="token punctuation">(</span>ctx<span class="token punctuation">,</span> http<span class="token punctuation">.</span>MethodPost<span class="token punctuation">,</span>
blueskyPDSURL<span class="token operator">+</span><span class="token string">"/xrpc/com.atproto.repo.createRecord"</span><span class="token punctuation">,</span> <span class="token operator">...</span><span class="token punctuation">)</span>
</code></pre>
<p>I want a new post each time (hence the new <code>rkey</code>), but also need to guard against duplicate posts (this is called in every syndication dispatch):</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">var</span> dedupeInFlight <span class="token punctuation">=</span> river<span class="token punctuation">.</span>UniqueOpts<span class="token punctuation">{</span>
ByArgs<span class="token punctuation">:</span> <span class="token keyword">true</span><span class="token punctuation">,</span>
ByState<span class="token punctuation">:</span> <span class="token punctuation">[]</span>rivertype<span class="token punctuation">.</span>JobState<span class="token punctuation">{</span>
rivertype<span class="token punctuation">.</span>JobStatePending<span class="token punctuation">,</span>
rivertype<span class="token punctuation">.</span>JobStateScheduled<span class="token punctuation">,</span>
rivertype<span class="token punctuation">.</span>JobStateAvailable<span class="token punctuation">,</span>
rivertype<span class="token punctuation">.</span>JobStateRunning<span class="token punctuation">,</span>
rivertype<span class="token punctuation">.</span>JobStateRetryable<span class="token punctuation">,</span>
<span class="token punctuation">},</span>
<span class="token punctuation">}</span>
</code></pre>
<p><code>ByArgs</code> ensures uniqueness by checking the job's args, <code>ByState</code> monitors the queue state and discards duplicate syndication attempts from the UI. Retryable is in the list to prevent a second insert while a job is waiting to auto-retry after a transient failure.</p>
<aside>
<p>These guards aren't unique to Bluesky but are worth mentioning.</p>
</aside>
<hr/>
<p>With all of that in place — the manual testing, random debug posts, reading of documentation, re-reading of documentation, cycles of frustration and endless deployments — I now sport a shiny publication bar for shared posts on Bluesky.</p>
<div class="footnotes" role="doc-endnotes"><hr/><ol><li id="fn:1"><p>More on that, bit by bit, later. ↩</p>
</li><li id="fn:2"><p>I'll probably move to a self-hosted PDS eventually, but the complexity of this implementation scared me away from doing so for now. ↩</p>
</li></ol></div></div>📝 2026-07-07 18:43: Guinea fowl keets are doing great too. All 5 are loving life with their foster... - Kev Quirkhttps://kevquirk.com/2026-07-07-18432026-07-07T17:43:00.000Z<p>Guinea fowl keets are doing great too. All 5 are loving life with their foster mums.</p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-07-1843/1000010240.webp" alt="1000010240" /></p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-07-1843/1000010242.webp" alt="1000010242" /></p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-07-1843/1000010243.webp" alt="1000010243" /></p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-07-1843/1000010246.webp" alt="1000010246" /></p> <div class="email-hidden">
<hr />
<p>Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️</p>
<p>You can <a href="mailto:19gy@qrk.one?subject=%F0%9F%93%9D%202026-07-07%2018%3A43">reply to this post by email</a>, or <a href="https://kevquirk.com/2026-07-07-1843#comments">leave a comment</a>.</p>
</div>📝 2026-07-07 18:34: We now have 3 chicks - two white and one black. - Kev Quirkhttps://kevquirk.com/2026-07-07-18342026-07-07T17:34:00.000Z<p>We now have 3 chicks - two white and one black.</p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-07-1834/1000010239.webp" alt="1000010239" /></p> <div class="email-hidden">
<hr />
<p>Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️</p>
<p>You can <a href="mailto:19gy@qrk.one?subject=%F0%9F%93%9D%202026-07-07%2018%3A34">reply to this post by email</a>, or <a href="https://kevquirk.com/2026-07-07-1834#comments">leave a comment</a>.</p>
</div>CNN and CNBC promote gambling to make a cheap buck - Werd I/O6a4d3079ffd4800001dcb3dd2026-07-07T16:59:37.000Z<p>Link: <a href="https://www.publicnotice.co/p/kalshi-cnn-cnbc?ref=werd.io"><em>How Kalshi infects the news, by Aaron Rupar and Judd Legum in Public Notice and Popular Information</em></a></p><p>Kalshi’s deals with newsrooms seem to be paying dividends for the company:</p><blockquote>“Since December CNBC has published 58 articles that do little more than advertise the existence of a Kalshi market related to a news event. […] Since April, CNBC has employed a <a href="https://www.cnbc.com/davis-giangiulio/?ref=werd.io">dedicated reporter</a> to produce these articles. CNBC also maintains a <a href="https://www.cnbc.com/markets/prediction-markets/?ref=werd.io">page</a> on its website featuring Kalshi prediction markets selected by CNBC editors, along with its web coverage. […] In at least 22 cases, CNBC has written about Kalshi and not disclosed its financial conflict.”</blockquote><p><a href="https://www.axios.com/2025/12/02/cnn-kalshi-prediction-market-data?ref=werd.io">CNN doesn’t pay for access</a>, and instead is <a href="https://www.newyorker.com/news/the-lede/americas-betting-craze-has-spread-to-its-news-networks?_sp=bc5180fe-4e8b-4c96-a107-44eca35ac553.1783442134229&ref=werd.io">paid to exclusively promote Kalshi</a>. <a href="https://www.publicnotice.co/p/kalshi-cnn-cnbc?ref=werd.io">CNBC reporting carries a disclosure</a> which states that its relationship goes further: “CNBC and Kalshi have a commercial relationship that includes customer acquisition and a minority investment.” CNBC will gain financially if its coverage leads to more signups or a growth in Kalshi’s valuation. CNN’s is a simpler paid placement, but both deals are aggressive ways for Kalshi to compete with Polymarket, which has been making similar deals with newsrooms like Yahoo Finance.</p><p>This is even happening when markets are not significant enough to be newsworthy. <a href="https://www.newyorker.com/news/the-lede/americas-betting-craze-has-spread-to-its-news-networks?_sp=bc5180fe-4e8b-4c96-a107-44eca35ac553.1783442134229&ref=werd.io">As the New Yorker noted in December</a>:</p><blockquote>“When Enten lauded the benefits of analyzing betting odds, on air the other day, he failed to mention that only several hundred thousand dollars had been bet on that particular market. Kalshi’s odds provided good fodder for television, but, statistically speaking, they didn’t say much.”</blockquote><p>It reminds me of the deals Twitter made with newsrooms relatively early in its life. Suddenly, almost out of nowhere, anchors read out tweets on the news, and shows promoted their official Twitter accounts over their websites. This didn’t happen organically: Twitter partnerships teams made deals behind the scenes to ensure their product was showcased well. It was one of the first times that a web startup impactfully executed on a media strategy, and startups have built on that pattern ever since.</p><p>Here, rather than serving a social network, money is changing hands for newsrooms to promote gambling markets — and in CNBC’s case, they will make more money if more people gamble. It’s obviously weirder, and the incentives here would pull at traditional newsroom ethics in an uncomfortable way even if the adequate disclosures were published. This comes at an unfortunate time when trust in news is falling quickly, and newsrooms like CNN are increasingly seen as serving their owners rather than bastions of trustworthy reporting. These Kalshi deals are weird, and an obvious conflict of interest that will likely drive people to trust the news even less than they do today.</p><p>The Reuters Institute’s 2026 Digital News Report found that <a href="https://reutersinstitute.politics.ox.ac.uk/digital-news-report/2026?ref=werd.io">70% of respondents think media owners and corporate parents exert undue influence on the news</a>. As more of these sorts of deals are made, and as trust in news continues to decline, newsrooms are going to need to more overtly state that their coverage is free from this sort of sponsored content. Stronger, more transparent ethics statements, and louder conversations about how reporting decisions are made, will help some newsrooms to explain how they stand apart from these dynamics. In the meantime, CNN and CNBC are helping to drive trust in media into the gutter.</p>How I lost my phone - Joel's Log Fileshttps://joelchrono.xyz/blog/lost-phone2026-07-07T15:20:00.000Z<p>Yesterday after work I got in the bus back home and proceeded to happily play on my <a href="/blog/nintendo-3ds/">Nintendo 3DS</a> for the whole trip. I was about to finish a stage of <em>Fire Emblem Awakening</em>, leveling up a lot of characters, dodging attacks and surviving hits, all nice and well!</p>
<p>Then the ride was over, I noticed I was the last person left in the vehicle, I rushed to the exit and stepped out to the street. Ten seconds later, I realized I did not have my phone with me.</p>
<p>My workplace provides us with free transportation, so there are contacts and people in charge who may be able to help me out, I just didn’t know who they are.</p>
<p>Thankfully, I didn’t lose my work phone. I started messaging people from there, a coworker who may know someone, or another peer who uses the same bus.</p>
<p>I manage to join the WhatsApp group of the bus route, where I can finally contact the transport manager, but I got no reply back.</p>
<p>I decide to let my boss know, to see if he could help me find someone else to ask around, he told me he would look into it as well. I was stressed out of my mind.</p>
<p>Running back home like a madman, I dash to my room and my laptop and check to see if Google’s <em>Find My Device</em> works—I remembered my Google account was never logged in. Sometimes you wonder if a degoogled life is even worth it. LOL</p>
<p>Both Signal and WhatsApp can work without having the device online nowadays, so I was able to message a bunch of people and let them know I could be offline for a while, I may even have to change phone numbers because of this, but I wasn’t fully giving up yet.</p>
<p>Another advantage I had is that I still keep my older Xiaomi phone, and even though the power button was acting up, it decided to behave for once and turn on after a charge. I was able to log-in to my bank apps from there and keep an eye on them. That also meant that not every single picture was lost.</p>
<p>Oh, how many things I would have lost… My chat history, all the photos I’ve taken on my Nothing 3(a), my AntennaPod stats, one of my banking apps. I’d have to redownload all my music too, which is annoying.</p>
<p>Eventually, my boss tells me he gave my contact info to someone in HR, and few more minutes later I’m sent photos of the lost device. That brings me relief, as I’m told the transport supervisor has it and that I can check with him tomorrow—which is today now.</p>
<p>My phone is safe and sound, and now it’s back in my hands, peace of mind returns.</p>
<p>Clearly, it’s a bad idea not to check where your belongings are before getting out of a bus. It’s also not ideal to get too distracted by a videogame or to rush things—definitely double-checking from now on. This was honestly one of the most ideal scenarios ever, there are a lot of things that could have gone very very wrong.</p>
<p>I could have left my work phone there as well, my boss could have ignored me completely, I could have no access to Signal/WhatsApp on my laptop, or lack a secondary phone to check my bank accounts, I could have taken a public bus with no ties to my workplace, the bus driver could have kept the device for himself, or the bus manager, a simple lie is all it takes. Thank God it’s all over now, and nothing was lost.</p>
<p>Later I realized I <em>do</em> have <em>Find My Device</em>, I was using a blank Google account untied from anything but that specific phone, so I forgot. I did have the login info backed up, so in the future I can make use of this. Although it would probably have been stressful to see my phone somewhere without being able to do much about it.</p>
<p>Make your backups guys! And also, if you know some good way to track your device without using Google services, let me know!</p>
<p>At the very least, I guess that being able to not be glued to my phone for ten seconds is a good thing, so… <em>win?</em></p>
<p>This is day 91 of <a href="https://100daystooffload.com">#100DaysToOffload</a></p>
<p>
<a href="mailto:me@joelchrono.xyz?subject=How I lost my phone">Reply to this post via email</a> |
<a href="https://fosstodon.org/@joel/116879394050333528">Reply on Fediverse</a>
</p>📝 2026-07-07 07:06: First two chicks! - Kev Quirkhttps://kevquirk.com/2026-07-07-07062026-07-07T06:06:00.000Z<p>First two chicks!</p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-07-0706/1000010230.webp" alt="1000010230" /></p> <div class="email-hidden">
<hr />
<p>Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️</p>
<p>You can <a href="mailto:19gy@qrk.one?subject=%F0%9F%93%9D%202026-07-07%2007%3A06">reply to this post by email</a>, or <a href="https://kevquirk.com/2026-07-07-0706#comments">leave a comment</a>.</p>
</div>Finished reading Wasteland Warlords 1 - Molly White's activity feed6a4c7ae291f30f1ebeded41f2026-07-07T04:04:50.000Z<article class="entry h-entry hentry"><header><div class="description">Finished reading: </div></header><div class="content e-content"><div class="book h-entry hentry"><a class="book-cover-link" href="https://www.mollywhite.net/reading/books?search=Wasteland%20Warlords%201"><img class="u-photo book-cover" src="https://m.media-amazon.com/images/S/compressed.photo.goodreads.com/books/1698673897i/201044505.jpg" alt="Cover image of Wasteland Warlords 1" style="max-width: 300px;"/></a><div class="book-details"><div class="top"><div class="series-info"><i>Wasteland Warlords</i> series, book <span class="series-number">1</span>. </div><div class="title-and-byline"><div class="title"><i class="p-name">Wasteland Warlords 1</i> </div><div class="byline">by <span class="p-author h-card">James A. Hunter</span> and <span class="p-author h-card">Eden Hudson</span>. </div></div><div class="book-info">Published <time class="dt-published published" datetime="2023">2023</time>. 150 pages. </div></div><div class="bottom"><div class="reading-info"><div class="reading-dates"> Started <time class="dt-accessed accessed" datetime="2026-07-06">July 6, 2026</time>; completed July 6, 2026. </div></div></div></div></div><img src="https://www.mollywhite.net/assets/images/placeholder_social.png" alt="Illustration of Molly White sitting and typing on a laptop, on a purple background with 'Molly White' in white serif." style="display: none;"/></div><footer class="footer"><div class="flex-row post-meta"><div class="timestamp">Posted: <time class="dt-published" datetime="2026-07-07T04:04:50+00:00" title="July 7, 2026 at 4:04 AM UTC">July 7, 2026 at 4:04 AM UTC</time>. </div></div><div class="bottomRow"><div class="tags">Tagged: <a class="tag p-category" href="https://www.mollywhite.net/reading/books?tags=fantasy" title="See all books tagged "fantasy"" rel="category tag">fantasy</a>, <a class="tag p-category" href="https://www.mollywhite.net/reading/books?tags=litrpg" title="See all books tagged "litRPG"" rel="category tag">litRPG</a>, <a class="tag p-category" href="https://www.mollywhite.net/reading/books?tags=post_apocalyptic" title="See all books tagged "post-apocalyptic"" rel="category tag">post-apocalyptic</a>. </div></div></footer></article>Purpose, mission, values - Johnny.Decimalhttps://johnnydecimal.com/blog/0229-purpose-mission-values/2026-07-07T02:00:00.000Z<p>After <a href="https://johnnydecimal.com/blog/0228/">discussing this at the last small business meeting</a>, we've thought about and written down our purpose, mission, and values. I'll put a copy here, and add them to their proper home which is our <a href="https://johnnydecimal.com/support/about-legal/policies">policies page</a>. If it's much later than July 2026 when you read this, you should check there for the latest version. This post won't change.</p>
<p>With thanks to <a href="https://oldstructures.nyc">Don</a> and <a href="https://lovettsundries.com">Jeff</a> for the interesting discussion. Don kindly shared a section of his company manual with us, which really helped.</p>
<hr>
<h2 id="purpose">Purpose</h2>
<blockquote>
<p><em>Why do we exist?</em></p>
</blockquote>
<p>In the 1980s, it was possible to live your life without using a computer. You might have made furniture by hand and driven it to a store that sold it for you. You paid the road toll with coins and received a paper receipt, your invoice was hand-typed and recorded in a ledger, and your tax return was filled in with a pen. This generated a small handful of physical documentation which was easily managed using a concertina folder: a basic skill you had been taught at school.</p>
<p>The volume of information was manageable. <strong>You could find anything, quickly and easily</strong>.</p>
<p>Today, that same carpenter – whose job has not changed – needs online accounts for her eToll account and the invoicing software that interfaces with the furniture store and her accountant. Both of these send important documents to her Gmail, which is full of transactional email from the 142 other organisations that she routinely deals with, and the social media accounts that she needs to maintain. Parking her 1982 Land Cruiser outside the furniture store now requires an app.</p>
<p>To help her manage this, her computer provides tools no more sophisticated than <code>Create New Folder</code>. It allows her to save anything anywhere she wants. It positively encourages the duplication of information. At no point was she given any training in these systems, which change constantly and without warning. The people who understand them charge hundreds of dollars an hour.</p>
<p>The volume of information is unmanageable. <strong>Nobody can find anything any more</strong>. This is stressful and inefficient. Computers were meant to make our lives easier. In this regard, they have done the opposite.</p>
<h2 id="mission">Mission</h2>
<blockquote>
<p><em>What are we going to do about it?</em></p>
</blockquote>
<p>We help people manage their information. This information is mostly, but not exclusively, on a computer.</p>
<p>We do this using the Johnny.Decimal system. It's a simple way to organise any information. It provides:</p>
<ol>
<li>A simple structure that uses numbers instead of the alphabet.</li>
<li>An index of your content that you manage.</li>
<li>Training, software, and support.</li>
</ol>
<p>We will be the training you never got: we'll show you how to store things so that you can find them again.</p>
<p>We can't make it so that our carpenter doesn't have to use a computer. And that's not our goal: computers <em>are</em> more efficient. Filing your taxes online is better than doing them with a pen.<sup><a href="#user-content-fn-parking-meter" id="user-content-fnref-parking-meter" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">1</a></sup></p>
<p>Our mission is to make it so that when she sits down in front of her computer, it is with a sense of calm, not dread. We want her to be able to find her stuff with more confidence, and less stress.</p>
<h2 id="values">Values</h2>
<blockquote>
<p><em>How will we behave while we do it?</em></p>
</blockquote>
<p>Johnny.Decimal is two humans: Johnny and his partner Lucy. We have no desire to grow. Becoming a big corporation wouldn't make us happy. We quit our jobs to do this <em>because</em> we love it; if we stop loving it, then what was the point?</p>
<p>This makes us a deeply 'Personal Business'.<sup><a href="#user-content-fn-personal" id="user-content-fnref-personal" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">2</a></sup> You're dealing <em>with us</em>, and we with you.</p>
<p>We love the satisfaction that comes from helping an individual to be more organised. We love our community. We love the sense of worth that comes from creating something good using our own brains.</p>
<p>We don't seek to be incredibly good 'at business'. We don't follow the same old (dark) patterns, the same old (sleazy) techniques. Surely this means we earn less than we could, that we remain smaller than we might otherwise have grown. We're okay with that.</p>
<p>Because we, personally, seek something simple: we'd like to be able to buy a house and have chickens in the back yard. We think we can achieve that by making good things that people like, and selling them for an honest price. Let us know how we're doing.</p>
<h3 id="your-values">Your values</h3>
<blockquote>
<p><em>How would we like you to behave towards us?</em></p>
</blockquote>
<p>Company values statements typically don't turn it around to face the customer. But this is our company so we'll do whatever we like. ;-)</p>
<p>I spoke above about how we're proud to be a 'Personal Business'. But please remember that we are <em>still a business</em>. This is how we earn our living.</p>
<p>There is a curious behaviour that I have observed over the years.</p>
<ol>
<li>Everyone seeks to escape 'the rat race'. This is idolised as the highest form of being: to quit one's job, to 'go indie'.</li>
<li>So one quits one's job and 'goes indie', forfeiting the steady wage.</li>
<li>Now 'a creator', one needs to sell something in order to pay the rent and eat. (Capitalism! Who knew?)</li>
<li>People react to this 'sell-out'. How dare you sell the product of your mind? Information should be free!</li>
</ol>
<p>My ask of you is this: notice this reaction in your own mind. <strong>Be charitable in your assumptions</strong>.<sup><a href="#user-content-fn-hanlon" id="user-content-fnref-hanlon" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">3</a></sup> If in doubt, ask the individual before posting about it on social media. We all need to earn money. You might do it by going to work. We do it by selling stuff online.</p>
<div data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-parking-meter">
<p>We're yet to be sold on the utility of an app over coins when paying for parking. <a href="#user-content-fnref-parking-meter" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-personal">
<p><a href="https://www.are.na/editorial/personal-business">Are.na / Personal Business by Charles Broskoski</a>. It's a great article, please read it. <a href="#user-content-fnref-personal" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-hanlon">
<p><a href="https://en.wikipedia.org/wiki/Hanlon's_razor">Hanlon's razor</a> providing a useful heuristic: "Never attribute to malice that which is adequately explained by stupidity." <a href="#user-content-fnref-hanlon" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
</ol>
</div>Finished reading Kiosk Kingdom - Molly White's activity feed6a4c56646337b1f9eb5113992026-07-07T01:28:47.000Z<article class="entry h-entry hentry"><header><div class="description">Finished reading: </div></header><div class="content e-content"><div class="book h-entry hentry"><a class="book-cover-link" href="https://www.mollywhite.net/reading/books?search=Kiosk%20Kingdom"><img class="u-photo book-cover" src="https://m.media-amazon.com/images/S/compressed.photo.goodreads.com/books/1751370683i/237735252.jpg" alt="Cover image of Kiosk Kingdom" style="max-width: 300px;"/></a><div class="book-details"><div class="top"><div class="series-info"><i>Discount Dan's Backroom Bargains</i> series, book <span class="series-number">3</span>. </div><div class="title-and-byline"><div class="title"><i class="p-name">Kiosk Kingdom</i> </div><div class="byline">by <span class="p-author h-card">James A. Hunter</span>. </div></div><div class="book-info">Published <time class="dt-published published" datetime="2026-03-31">March 31, 2026</time>. 680 pages. </div></div><div class="bottom"><div class="reading-info"><div class="reading-dates"> Started <time class="dt-accessed accessed" datetime="2026-06-25">June 25, 2026</time>; completed July 6, 2026. </div></div></div></div></div><img src="https://www.mollywhite.net/assets/images/placeholder_social.png" alt="Illustration of Molly White sitting and typing on a laptop, on a purple background with 'Molly White' in white serif." style="display: none;"/></div><footer class="footer"><div class="flex-row post-meta"><div class="timestamp">Posted: <time class="dt-published" datetime="2026-07-07T01:28:47+00:00" title="July 7, 2026 at 1:28 AM UTC">July 7, 2026 at 1:28 AM UTC</time>. </div></div><div class="bottomRow"><div class="tags">Tagged: <a class="tag p-category" href="https://www.mollywhite.net/reading/books?tags=fantasy" title="See all books tagged "fantasy"" rel="category tag">fantasy</a>, <a class="tag p-category" href="https://www.mollywhite.net/reading/books?tags=humor" title="See all books tagged "humor"" rel="category tag">humor</a>, <a class="tag p-category" href="https://www.mollywhite.net/reading/books?tags=litrpg" title="See all books tagged "litRPG"" rel="category tag">litRPG</a>. </div></div></footer></article>Once again, we failed to focus - Johnny.Decimalhttps://johnnydecimal.com/blog/0226-we-failed-to-focus/2026-07-07T01:00:00.000Z<blockquote>
<p><strong>Editorial note:</strong> This one was drafted a while back, and things have changed. I'll publish it mostly as originally written; see the adjacent posts for more context.</p>
</blockquote>
<p>We've done it again. And we're sorry – Lucy in particular <em>hates</em> unfinished business – but we started a thing that we shouldn't have, so we're going to cancel it. <del>Two things! Those</del> That thing being our 'Everyday Obsidian' <del>and 'Online security essentials'</del> mini-courses.</p>
<blockquote>
<p><strong>Update:</strong> We're keeping the security thing for now. I have ideas how it can be an important part of how we help small business. I leave it here to show how these decisions aren't easy.</p>
</blockquote>
<p>Once again this is a textbook case of not focusing on what's important. As a <em>very</em> small business, we need to guard our time. We started these things without really thinking them through. At the time they were a welcome distraction: we'd just come out of a months-long zone of focus, <a href="https://johnnydecimal.com/blog/0208-this-week-2026-05-25#what-we-did-last-week">merging the two websites</a>. <del>And I really would love to do that online security thing one day. But in hindsight, they were wrong for now.</del></p>
<h2 id="whats-our-purpose">What's our purpose?</h2>
<p>We mock 'mission statements', but their value just became apparent to me. We've never written down exactly what it is we're here for: <strong>why do we exist?</strong></p>
<p>We still haven't written that down, but if I think out loud it's something like: <em>people need help organising stuff, and we have an interesting method of doing that, so we should try to help them</em>.</p>
<blockquote>
<p><strong>Update:</strong> I drafted this post weeks ago. Since then we've written down our <a href="https://johnnydecimal.com/blog/0229/">purpose, mission, and values</a>.</p>
</blockquote>
<p>Do <em>explaining Obsidian's features</em> or <em>helping you not get hacked online</em> fit into that? The former, kinda, maybe. But I'm not even that good at Obsidian! You want Obsidian advice there are tens of thousands of hours of that already on YouTube. The latter – having thought on it, I see a place where that fits into what we do.</p>
<p>When we're back at work later this week this is one of the first things we'll be doing: writing down short, practical, human answers to the following questions.</p>
<ol>
<li>Why do we exist? (Purpose)</li>
<li>What are we going to do about it? (Mission)</li>
<li>How do we behave while we do it? (Values)</li>
</ol>
<h2 id="use-this-as-a-filter">Use this as a filter</h2>
<p>I think the reason we mock large corporations' mission statements is that they feel like platitudes that someone's had engraved on a plate and then never read again. Certainly that's how I felt at basically every job I had.<sup><a href="#user-content-fn-think-different" id="user-content-fnref-think-different" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">1</a></sup></p>
<p>So how do we make sure that these statements, once codified, serve their purpose? I haven't implemented this yet, but it feels simple enough.</p>
<ol>
<li>Create a 'new <a href="https://johnnydecimal.com/blog/0225/">project</a>' template in our JDex.</li>
<li>This template has a checkbox that asks you to consider the project in the context of this just-documented purpose, mission, and values. Does it align with them?</li>
</ol>
<p>Of course you need to actually give some thought to this whenever you create a new project. Boxes like this can be something you check without thought once it becomes routine. This is something that the two of us should actively discuss together.</p>
<p>This is a work-in-progress. I'll report back.</p>
<div data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-think-different">
<p>Most such 'mission statements' can be summed up as: <em>think different and act bravely</em>. Well I tried that and let me tell you, that is <em>not</em> what your corporate middle-manager wants. <a href="#user-content-fnref-think-different" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
</ol>
</div>June-boree! - Hey, it's Jason!https://grepjason.sh/2026/june-boree-trophees2026-07-07T00:00:00.000ZIt's that time of year! Behold! June-boree!Wonders of Web Weaving, Episode 9 - James' Coffee Bloghttps://jamesg.blog/2026/07/07/www-92026-07-07T00:00:00.000Z
<p><a href="https://web-weaving.jamesg.blog/9" rel="noreferrer">The ninth episode of Wonders of Web Weaving is out</a>:</p><blockquote>In Episode 9, I chat with Rachel, the author of <a href="https://kwon.nyc">kwon.nyc</a> and curator of "<a href="https://projects.kwon.nyc/internet-is-fun/">The internet used to be* fun *(It still is, but it used to be, too)</a>". We talk about, among other things, metaphors for the web, creative mediums on the web, physical creative spaces, and more.</blockquote><p>I hope you enjoy the episode!</p><p><a href="https://web-weaving.jamesg.blog/subscribe/" rel="noreferrer"><em>Wonders of Web Weaving also has an RSS feed</em></a><em> you can use to follow along from wherever you get your podcasts.</em></p><script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a177ebca7c208b9c',t:'MTc4MzQzODU0Nw=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
<a class="tag" href="https://kwon.nyc">kwon.nyc</a>
<a class="tag" href="https://projects.kwon.nyc/internet-is-fun/">The internet used to be* fun *(It still is, but it used to be, too)</a>
<a class="tag" href="https://web-weaving.jamesg.blog/9">The ninth episode of Wonders of Web Weaving is out</a>
<a class="tag" href="https://web-weaving.jamesg.blog/subscribe/">Wonders of Web Weaving also has an RSS feed</a>
📝 2026-07-06 21:45: Two of the eggs are starting to hatch and we can hear multiple chicks cheeping... - Kev Quirkhttps://kevquirk.com/2026-07-06-21452026-07-06T20:45:00.000Z<p>Two of the eggs are starting to hatch and we can hear multiple chicks cheeping away. Exciting! 🐣</p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-06-2145/1000010211.webp" alt="1000010211" /></p> <div class="email-hidden">
<hr />
<p>Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️</p>
<p>You can <a href="mailto:19gy@qrk.one?subject=%F0%9F%93%9D%202026-07-06%2021%3A45">reply to this post by email</a>, or <a href="https://kevquirk.com/2026-07-06-2145#comments">leave a comment</a>.</p>
</div>Return to the gym, 3DS theming, Mexico lost - W27 - Joel's Log Fileshttps://joelchrono.xyz/blog/w272026-07-06T17:30:00.000Z<p>Mexico was knocked out of the World Cup by England. You have no idea how much hope I had for this squad, the quality and resiliance Mexico had shown. Unfortunately we were unable to take advantage of the locality and it only took a couple of mistakes for England to put us in a very bad situation, two goals in just a couple minutes, and a third penalty due to one of the only errors our goalkeeper ever did. Quiñones helped us stay afloat, and a killer penalty kick from Jimenez too, but the game devolved into a battle of trying to get the ball in the area, when it was all filled by the brittish. The weirdest decision for me was taking out the creativity of Mora and the power of Quiñones, and relying on the same play.</p>
<p>Even with that, Mexico fought hard, so many attempts, many of them would have gone in, but the goalkeeper did his job very well. And now we lose, but it’s not the same as always for me. I believe in the new generation. Mora has a long way to go, I would have wanted to see more of la Hormiga, the inheritor of Chicharito’s 14. Rafa Marquez is the manager now, I’m hopeful that the Kaiser will do great for the future.</p>
<p>Oh right, week notes. Let’s see.</p>
<ul>
<li>
<p>💪 I have returned to the gym! It was a long hiatus full of laziness and overeating, I went up two kilos since the last time I checked, so I decided to go back and try again to take better care of myself. I continue to be annoyed by the need to go at all, it just kinda feels like a trend for me and a bit of a timewaste. I really need to get that bycicle commute going, it has the potential to really change things for the bettter for me.</p>
</li>
<li>
<p>🎮 The <em>Nintendo 2DS XL</em> arrived and it was a bit dissapointing, if I’m honest. While the design is fun, the plastic feels much cheaper and the structural integrity feels lesser than the New 3DS XL. I packaged it up and requested to return it, I only turned it on once and didn’t even try any games on it.</p>
</li>
<li>
<p>🍿 Invited my friend to another movie! This time we arrived earlier and went window shopping for a bit, and also played some card games! We watched <em>Supergirl</em>, and it was honestly kind of weird. All in all, the point was to spend some time together and it was alright.</p>
</li>
<li>🚲 This weekend’s bike ride was a very functional one! I went a bit later than my usual morning because I wanted to visit a couple of places. I actually carried a backpack this time too!
<ul>
<li>I headed to a delivery spot to return the <em>Nintendo 2DS XL</em>, I picked up a couple packages as well, then</li>
<li>I went to the local game shop to look for any 3DS games, I had no carts and I wanted at least one! I chose the <em>Ocarina of Time</em> remake.</li>
<li>I was supposed to go to the bike shop to buy fenders and a rack, but it was late already so it didn’t happen.</li>
</ul>
</li>
<li>
<p>🍜 Went to a Japanese restaurant and had some ramen, this was a night out with a few friends, it was really nice! We pretty much only go here when these specific friends are in town, so I always enjoy it.</p>
</li>
<li>
<p>🎨 Spent a couple hours figuring out how to create themes for the Nintendo 3DS, and ended up making two! Of course they are based on some epic locations from <em>Chrono Trigger</em>, I will write a blogpost detailing the process and have already uploaded them to <a href="https://themeplaza.art">Theme Plaza</a>, waiting for approval!</p>
</li>
<li>📱 I rarely use mainstream social media, but during the World Cup I keep visiting it to see the memes and the euphoria, and now the sadness of losing—and memes asking Haaland to avenge us. Until next time, México.</li>
</ul>
<p><img src="/assets/img/blogs/2026-07-06-week.webp" alt="Collage of the Week" /></p>
<h2 id="gaming">Gaming</h2>
<ul>
<li>
<p><strong>Fire Emblem Awakening</strong> continues to be at the top for my gaming time, the story has picked up quite a bit, but the mechanics alone, and the challenge of each chapter have been formidable. I am currently on chapter 13, and some revelations have been made that caught my eye. I also love my party quite a bit now! I’ve managed to create a pretty decent team but I’m also trying to level up some weaker characters and also level up the support between characters, I love the conversations that happen between them. Looking forward to what this game has next!</p>
</li>
<li>
<p><strong>Fire Emblem The Sacred Stones</strong> was played for a second. I remembered that I have a lot of handhelds, and my Miyoo Mini Flip had this one loaded up, I wanted to try a battle but didn’t even get far enough for that yet. I opened the game, saw the opening sequence, and got busy with something else.</p>
</li>
<li>
<p><strong>Super Smash Bros Ultimate</strong> saw some action! and the best player of my friend group has started to struggle with the younglings, we are getting old, RIP.</p>
</li>
<li>
<p><strong>Worms W.M.D</strong> - We did one battle of this as well, it was a fun time! We kinda forgot how to play Worms but picked things up in the end.</p>
</li>
</ul>
<h2 id="watching">Watching</h2>
<ul>
<li>
<p><strong>Avatar The Last Airbender (Live Action Season 2)</strong> - We are in the Earth Kingdom and they are actually pulling off a couple neat moments in great ways! Like, ways that could have worked on the original series rather well. It still has some very awkward things and bad pacing, but I’m invested a bit.</p>
</li>
<li>
<p><strong>Supergirl (2026)</strong> - Went to the movies with my friend and family to see this one. It was good, but it wasn’t great. It really has a lot to say about society and stuff, but it felt mixed, and the music selection wasn’t that great. Too convoluted, and nobody in my group resonated with it that much. I really really wanted to like it, and I do, it just didn’t quite stick the landing for me.</p>
</li>
<li>
<p><strong>Superman (2025)</strong> - Back at home, I thought I’d give a shot to the first movie in James Gunn’s DC universe. I enjoyed this one! It really questioned the nature of Superman and how it affects the politics of the world. Some moments felt over the top and had that characteristic style James Gunn has. I appreciate that the universe already exists and it doesn’t feel the need to hold our hand. However, the plot was predictable and with almost no surprises. Mr. Terrific was truly terrific though, great guy!</p>
</li>
<li>
<p><strong>Yes Day</strong> - I watched this movie ages ago and only recognized Electra, watching it again, I also recognized Jenna Ortega. What a weird family movie. Entertaining enough but there’s better ways to spend your time.</p>
</li>
</ul>
<h2 id="reading">Reading</h2>
<p><strong>Shikimori’s Not Just Cute</strong> is now complete! What a journey, a tiny bit rushed in the ending but still very nice! All in all this manga was a cozy read that had some great moments and hilarious situations. It was not life-changing or anything, but I’m glad it tied things nicely. I actually grew to love all the characters beyond the main couple quite a bit, which is something that got a bit overwhelming with <em>Komi Can’t Communicate</em>. The cast here was smaller but the roles were well defined and they all had a time to shine if only a bit. I will actually miss some of them! I ended up a little melancholic about it due to Mexico’s loss, and I am running out of romance manga to read too, which is a bit sad. Leave recommendations.</p>
<p>I didn’t read any of <em>The Expanse</em> at all because of the gaming I did.</p>
<h2 id="around-the-web">Around the Web</h2>
<h3 id="blog-posts">Blog posts</h3>
<ul>
<li><a href="https://gabz.blog/posts/a-new-nintendo-3ds-xl">A New Nintendo 3DS XL</a> - Gabz also has a 3DS! Who would have thought I’d inspire him to return to it? I am happy to have caused that in him and a few other people too, very cool.</li>
<li><a href="https://bell.bz/im-playing-the-zelda-games-in-timeline-order/">I’m playing the Zelda games in timeline order</a> - Oh look! Andy is playing Zelda, that’s cool. I should try playing through a whole franchise sometime, I guess I’m kind of close with <em>Resident Evil</em>…</li>
<li><a href="https://neilzone.co.uk/2026/07/being-excited-still-about-technology-in-2026/">Being excited still about technology in 2026</a> - Neil continues to remind us that technology still has some good things going on. It’s not all doom and gloom!</li>
<li><a href="https://pawelgrzybek.com/my-blog-is-not-on-the-at-protocol-standard-site-is-not-for-me">My blog is not on the AT Protocol, standard.site is not for me</a> - This. AT Protocol, to me, is not where it’s at, I added <a href="https://microformats.org/">microformats</a> and that feels much simple and universal to me, even then it feels a bit much, but it’s setup and forgotten and it’s okay!</li>
</ul>
<h3 id="youtube">YouTube</h3>
<ul>
<li><a href="https://youtu.be/dvAczAth9ms">How To Make Your Own Custom 3DS Themes / Kame-Editor</a> - This is the tutorial I followed to make my theme, although I had to figure out some things it didn’t explain especially for the image placement on static images (the tutorial is only about scrolling backgrounds).</li>
<li><a href="https://youtu.be/qeKJqnlF3UQ">The Reusable Water Bottle Trend is DEAD</a> - Making good purchasing decisions hasn’t been my forte this year, but at least I know I won’t fall for energetic drinks any time soon.</li>
<li><a href="https://youtu.be/QiEtHwCTPb0">Why Mexico v. England is already historic</a> - This article from the <em>Search Party</em> channel shared a lot of awesome details on the history of the Azteca Stadium! I recommend giving it a watch as it had a lot of details I didn’t know about at all.</li>
<li><a href="https://youtu.be/bue-lSUJr-g">For Real this Time!</a> - Someone seems to be working on a port of <em>Twilight Princess</em> for the Nintendo 3DS. I hope more GameCube games get ported to the handheld some day.</li>
<li><a href="https://youtu.be/QLkc_b1L9Ow">Dear Sony: You messed up</a> - It seems like physical discs for Play Station will stop being a thing. This is disaster, but kind of expected. I hope things change some day.</li>
</ul>
<p>This is day 90 of <a href="https://100daystooffload.com">#100DaysToOffload</a></p>
<p>
<a href="mailto:me@joelchrono.xyz?subject=Return to the gym, 3DS theming, Mexico lost - W27">Reply to this post via email</a> |
<a href="https://fosstodon.org/@joel/116874409962412108">Reply on Fediverse</a>
</p>Governance can prevent AI from being used to undermine democracy. But only if it has teeth. - Werd I/O6a4baf9b1f0a8b0001fa58532026-07-06T13:37:31.000Z<p>Link: <a href="https://www.techpolicy.press/in-geneva-the-world-can-anchor-ai-governance-in-free-expression/?ref=werd.io"><em>In Geneva, the World Can Anchor AI Governance in Free Expression, by Isabelle Anzabi in Tech Policy Press</em></a></p><p>I like that this is happening, will appreciate the recommendations that arise from it, and know with complete certainty that no major AI vendor will adhere to them unless they are forced:</p><blockquote>“The United Nations is convening its <a href="https://www.un.org/global-dialogue-ai-governance/en?ref=werd.io">Global Dialogue on Artificial Intelligence Governance</a> in Geneva July 6-7, and the stakes are high: what rights will anchor the future of how the world governs AI? Generative AI systems mediate, filter, and generate the information we encounter every day. They are, at their core, a technology of expression and access to information. How we govern them will have downstream effects on what people can say, seek, and know. Geneva is where we can get that right.”</blockquote><p>A previous UNESCO meeting recommended governance that “bars AI systems from being used for social scoring or mass surveillance, and  requires member states to ensure that AI actors respect rights in the AI lifecycle”. As we all know, that has <em>definitely</em> happened. Both AI vendors and the two countries that predominantly house them, the United States and China, are famously against surveillance and social scoring, and in favor of maintaining rights. Certainly none of them have, for example, used AI with <a href="https://www.npr.org/2026/01/23/nx-s1-5684185/doge-data-social-security-privacy?ref=werd.io">the most sensitive personal information government has access to</a>, or worked with the private sector to <a href="https://theconversation.com/us-government-ramps-up-mass-surveillance-with-help-of-ai-tech-data-brokers-and-your-apps-and-devices-277440?ref=werd.io">create a surveillance apparatus</a> for policing <a href="https://www.404media.co/elite-the-palantir-app-ice-uses-to-find-neighborhoods-to-raid/?ref=werd.io">that includes the broad detainment of immigrants</a>. Neither of them is famous for social scoring — in China by government or in the United States <a href="https://hai.stanford.edu/news/how-flawed-data-aggravates-inequality-credit?ref=werd.io">through private enterprise</a>. Job done!</p><p>Sarcasm aside, it’s important to have these meetings, and it’s important to continue to add pressure to protect rights and prevent abuses of this technology. But we should be clear-eyed about the fact that any recommendations almost certainly won’t actually be followed unless major nations that represent real dollars to these vendors enshrine them in law and hold the line when countries like the US apply pressure to undermine them.</p><p>It can’t just come down to <em>individual</em> governments. The linked article argues that we should also protect freedom of expression, in particular from government (although I’d argue we should also worry about the influence of wealthy private entities). Because these are black box systems, it’s vital that we prevent governments (or anyone with power) from opaquely tweaking their answers and outputs to benefit their agendas. They should not be allowed to be opaque systems; full transparency and auditability should be requirements. The danger is that, in the wrong hands, AI can be used to cement centralized, undemocratic power. To prevent this, ideally, organizations like the UN should apply real sanctions to nations that don’t obey transparency rules and tweak AI systems in service of undemocratic goals — but the UN’s history of doing this effectively is not strong.</p><p>Part of the problem is the “AI is the information technology of the future” framing used here. If you believe that AI is the future of information systems, you are also more likely to believe that your nation will miss out if you don’t embrace it completely. (That’s been the marketing: if you don’t jump into AI, you will be left behind.) But the reality is, of course, far more nuanced. AI will absolutely change industries like software engineering, and has already made an impact there, although it’s far from an existential transformation. I’m less receptive to the idea that it will transform entire nations. AI vendors need global markets more than global markets need AI. Governments should understand the power they have, and use it.</p>I'm just so bored of AI - Terence Eden’s Bloghttps://shkspr.mobi/blog/?p=713842026-07-06T11:34:18.000Z<p>I'm just so <em>bored</em> of talking about AI. It's like listening to vapers tell me how delicious their flavoured poison is.</p>
<p>Did you ever meet someone at university who'd just tried drugs for the first time? Listening to a stoner ramble on about their mystic crystal revelations is amusing for the first five minutes, but quickly gets tiresome. Wow! You got your little computer friend to automate calling your mum? Great job, mate! Can we talk about something interesting now?</p>
<p>Just as bad are the people telling me how bad smoking is for my health. Yes, I know! That's why I don't. Yes, I know that hanging out with friends while they smoke is bad for me - but loneliness is probably worse. In fact, you screaming about how every puff on a fag reduces my life expectancy by 37 seconds makes me want to inhale a whole cigar so I can die early enough to exit this tedious conversation.</p>
<p>If you want to enhance your consciousness / rot your brain with AI - go ahead. Just stop wanging on about it like you discovered the secret to eternal life.</p>
<p>And to the rest of you, stop pissing on people's chips. Haranguing and scolding have never convinced anyone of everything.</p>
<p>I can't fucking wait for this bubble to burst so we can go back to chatting shit about literally <em>anything</em> else.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=71384&HTTP_REFERER=Atom" alt width="1" height="1" loading="eager">Built for Exactly One - Kev Quirkhttps://kevquirk.com/built-for-exactly-one2026-07-06T11:00:00.000Z<div class="link card"><h2>Built for Exactly One</h2><p class="post-author">by Amit Gawande</p><p>Amit talks about what motivated him to build his custom blogging platform, Jot. It's an interesting read that resonated with me as it aligns with why I created Pure Blog.</p><p><a class="button" target="_blank" href="https://www.amitgawande.com/blog/2026/built-for-exactly-one/">Read post ➡</a></p></div>
<hr>
<blockquote>
<p>A month ago, this website moved to a custom engine that I built myself, one I call Jot. Why did I create it? Because I got tired of almost. Almost the right editor. Almost the right publishing flow. Almost the right feature set.</p>
<p><cite>-- Amit Gawande</cite></p>
</blockquote>
<p>Preach!</p>
<p>This is <em>exactly</em> why I started building <a href="https://pureblog.org">Pure Blog</a>, but the difference here is that I decided to publish it for everyone to use. Before doing so, I considered many of the same questions that Amit talks about in his post - I was concerned that the project would morph into a product for <em>everyone</em>, not just me.</p>
<p>Ironically, it's been exactly 5 months since I <a href="https://kevquirk.com/introducing-pure-blog">introduced Pure Blog</a> and since then I've done a shit tonne of work to it. But that wasn't driven by the people who use it. It was driven, almost exclusively, by me. Lots of people have contributed to Pure Blog, but there's hasn't been a single feature I've added that I won't get use from.</p>
<p>Actually, that's a lie. The <em>only</em> feature I've added that I wouldn't have if I'd kept Pure Blog private is <a href="https://docs.pureblog.org/changelog/#v2.2.0-%E2%80%94-27-march-2026">translations</a>. But I think that's fine, as it's the community who contribute those translations.</p>
<p>Anyway, I digress. I'm happy to see other bloggers forging their own path - I'd love to get a look at Jot to see what it does differently to Pure Blog, and if there's anything I could <del>copy</del> improve upon. Maybe one day Amit will release the source code for us to look at, but if he doesn't, I don't blame him.</p>
<p>As for my use of Pure Blog - it's <em>by far</em> the best thing I ever did from a blogging perspective. <em>Everything</em> is just how I want it, and in a place that makes sense to me. If others get use from it too (<a href="https://roll.pureblog.org/">and they do</a>) then all the better. But I'll keep developing Pure Blog in a way that makes sense to me.</p>
<p>Congrats, Amit. Welcome to the club.</p> <div class="email-hidden">
<hr />
<p>Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️</p>
<p>You can <a href="mailto:19gy@qrk.one?subject=Built%20for%20Exactly%20One">reply to this post by email</a>, or <a href="https://kevquirk.com/built-for-exactly-one#comments">leave a comment</a>.</p>
</div>IndieWebCamp Nürnberg 2026 - James' Coffee Bloghttps://jamesg.blog/2026/07/05/indiewebcamp-nurnberg-20262026-07-05T00:00:00.000Z
<p><figure><picture><img alt="All of the participants of IndieWebCamp Nürnberg, lined up in three rows, one row behind the other. I am in the middle of the back row, wearing my green and grey microformats t-shirt. " loading="lazy" src="https://editor.jamesg.blog/content/images/2026/07/1000px-iwc-nbg-2026-10.jpg" style=" max-width: 130%;"/></picture><div class="alt"><label><input aria-label="Toggle image alt text on screen" type="checkbox"/>ALT</label><div class="content">All of the participants of IndieWebCamp Nürnberg, lined up in three rows, one row behind the other. I am in the middle of the back row, wearing my green and grey microformats t-shirt. </div></div></figure></p><p>Last weekend I attended <a href="https://indieweb.org/2026/Nuremberg">IndieWebCamp in Nürnberg, Germany</a>, hosted at the wonderful <a href="https://tollwerk.de/">Tollwerk</a> offices. The two-day event was part of the larger Nürnberg Digital Festival happening across the city. The IndieWebCamp was held primarily in English. Over 23 people attended, many from Germany and some from further afield (including myself!).</p><p>On the first day, we organised discussion sessions in the BarCamp style, in which new participants are encouraged to propose sessions first, followed by regular participants, and then we all agree on where and when during the day to hold each session. On the second day, we made web pages and shared them!</p><p>Indeed, being a two-day event, <a href="https://indieweb.org/IndieWebCamp" rel="noreferrer">IndieWebCamps</a> have room to balance both discussing topics that interest participants as well as creating things on our websites. We also welcome remote participants, and so, in addition to the ~23 attendees we had, there were several people joining sessions from Zoom (and even helping to take notes, which was and is greatly appreciated!).</p><p>I joined the event both as a participant and as something of a co-organiser, helping to ensure Zoom rooms were set up, helping to open Create Day, archiving notes on the wiki, and more. <a href="https://tantek.com/">Tantek</a> and <a href="https://jkphl.is/">Joschi</a> both led organising, with the <a href="https://tollwerk.de/">Tollwerk</a> team also helping in so many ways.</p><p>Throughout the day, <a href="https://www.maxhaesslein.de/" rel="noreferrer">Max</a> took pictures using a camera connected to a thermal printer. All the pictures were monochrome, which created a terrific effect. I have included some of these pictures throughout the post. Thank you Max for taking such great pictures!</p><h2 id="day-one:-discussions">Day One: Discussions</h2><p>I am always surprised and delighted at the variety of discussion topics that come up at IndieWebCamp events. I think the range of discussion topics is in large part because new participants are encouraged to share ideas first. This ensures that we don’t keep talking about the same topics between events.</p><p>This time, we discussed everything from how to welcome more new people to the indie web, calendar subscriptions on the web, beautiful web design, and whether or not the way we will consume websites will permanently change. <a href="https://indieweb.org/2026/Nuremberg/Schedule">Notes for all of these sessions are archived on the IndieWeb wiki</a> in case you would like to take a look at what we discussed.</p><p>Whenever I join a discussion, I like to try and take notes. This started at IndieWebCamp Brighton in 2024 when I realised how much I love being the note-taker. Every discussion needs at least one note-taker, and I’m happy to help (although the more people who take notes, the more we are able to capture, and the more likely the notes are to reflect all that was discussed). I think I helped with note-taking in every session I joined, which was fun.</p><p>Two sessions that stood out to me were “<a href="https://indieweb.org/2026/Nuremberg/blogchat" rel="noreferrer">Archiving live sessions / co-creating</a>” and “<a href="https://indieweb.org/2026/Nuremberg/thenewweb" rel="noreferrer">Will the way we consume websites permanently change?</a>”. The first session, led by <a href="https://thedriftinghealer.com" rel="noreferrer">Una</a>, was about how to create discussion on a website after a live session (i.e. a group event). It was fascinating to think about how the role artefacts (i.e. recording of sessions, summaries of sessions) play in building community. Our discussion led to digital gardens and knowledge graphs, which gave us plenty of points of inspiration.</p><p>The discussion about whether the way we consume websites will permanently change was a fascinating one. Facilitated by <a href="https://noracudazzo.com/">Nora</a>, we discussed the transformations currently happening on the web as a result of AI, the long-term viability of browsers, and more. This session encouraged the optimist in me. As Casey Newton said in the movie Tomorrowland “I get things are bad, but what are we doing to fix it?”</p><p>Starting with an assessment of where we are – a solid foundation from which to start our discussion – we spoke about the 700,000+ people who run sites on Neocities, the long-lived role of information design, the role of the web in government services, media diets, and more. We were then left with many questions, among them:</p><ul><li>How to convince artists to join the web?</li><li>"How do I talk to my friends on there [the web]?”</li><li>Maybe lurkers have a healthier relationship with consumption?</li></ul><p>The notes from the session include a note that “James doesn't have the words to convince someone without a pre-existing interest to start a website.” This reflected my realisation that while I have so many words to say on the web, I don’t think I have successfully encouraged someone to create a website in direct conversation who hasn’t already said something that would indicate an interest in the web.</p><p>Now that I write this, maybe this website has encouraged others to start websites, in which case maybe I do have the words! But I think the note speaks to a discomfort I have: I wish more people had websites, but communicating their value is tricky. Maybe the best thing I can do to advocate for the web is keep doing what I’m doing – writing, talking about websites passionately, making cool things on the web – and be there for anyone and everyone who sees potential in this medium as I do.</p><h2 id="day-two:-creating">Day Two: Creating</h2><p>The second day of an IndieWebCamp event is <a href="https://indieweb.org/Create_Day">Create Day</a>, in which participants are invited to work on their personal websites for the day. Toward the end of the day, everyone – both in-person and remote attendees – are invited to demo what they built on their website if they would like to share what they made.</p><p>At the beginning of Create Day, I stood up to share what the day is all about. Looking back, it amazes me the ease with which I was able to stand up in front of a room and introduce what Create Day is all about. I am bewildered by how anxious I feel at times in social situations and yet how I can stand up in front of a room and talk to over a dozen people with few notes. My confidence has certainly come a long way over the last few years; there are still many places for me to grow, too.</p><figure><picture><img alt="Me standing up in front of the room presenting what Create Day is all about." loading="lazy" src="https://editor.jamesg.blog/content/images/2026/07/IMG_4686-Large.jpeg" style=" max-width: 130%;"/></picture><div class="alt"><label><input aria-label="Toggle image alt text on screen" type="checkbox"/>ALT</label><div class="content">Me standing up in front of the room presenting what Create Day is all about.</div></div></figure><p>The <a href="https://indieweb.org/what_to_make_at_IndieWebCamp">“what to make at IndieWebCamp” page on the wiki</a>, authored by Tantek, was a good starting point for me to present what to do on Create Day. As someone who has attended an IndieWeb event before and tried to take on a big project, the points “start easy” and “prioritise joy” were particularly resonant. These points were mentioned by several participants as being their guide to the day as time passed, which was a delight to hear.</p><p>I had a few ideas in mind of what I wanted to do on Create Day. I made a new <a href="https://jamesg.blog/bookshelf">bookshelf</a> page as my first project. The last version of the page had not been updated in several years. I decided that rather than try to list all of the books I have on my shelves, I wanted the new page to encapsulate my current interests and include a few select recommendations across different genres. I am happy with the page I made.</p><figure><picture><img alt="Me, with long hair, smiling, sitting at a desk with my right hand on my laptop and looking directly into the camera." loading="lazy" src="https://editor.jamesg.blog/content/images/2026/07/IMG_4676-Large.jpeg" style=" max-width: 130%;"/></picture><div class="alt"><label><input aria-label="Toggle image alt text on screen" type="checkbox"/>ALT</label><div class="content">Me, with long hair, smiling, sitting at a desk with my right hand on my laptop and looking directly into the camera.</div></div></figure><p>I also made an <a href="https://jamesg.blog/indiewebcamp">IndieWebCamp category</a> on my blog and added posts explicitly about or written at IndieWebCamp events. This blog post will go in that category.</p><p>Having started with easier projects, I had finished in the first hour or so. By coincidence, something happened that would give me enough to work on for the rest of the day, and beyond. Tantek mentioned he had updated his “daylists” page, which lists the names of Spotify daylists that have been generated for him based on his music tastes. I didn’t see the changes in Artemis, my web reader, which led me to ask why this was the case.</p><p>This led to my working on two changes. First, <a href="https://jamesg.blog/2026/07/04/processing-feed-items-without-urls">Artemis can now process feed items that don’t have URLs</a>, which was at the root of the problem. Second, because Tantek updates his daylists page every few months and backdates the posts that are added to when the daylist appeared in his Spotify, I wouldn’t see many of the items because Artemis shows posts ordered by creation date. This led me to work on a (still-work-in-progress) feature that would display new items in a feed with a tag like “post title [2 days prev.]” to indicate that a post has been newly discovered but was published with a date equal to N days ago. This turned out to be a complex feature and so I am still working on the logic, but I made good progress during the event.</p><p>After several hours of making – and a wonderful salad lunch prepared by the Tollwerk team – we all came together for demos. It was delightful to see what everyone had made: new designs for a portfolio website, an interactive web page that lets you create a resonance signature based on your humming, improvements to photo grids on websites, and more. In total, we had ~17 demos, including demos presented by remote participants.</p><h2 id="heat-safety">Heat safety</h2><p>A few days before the event, myself, another event organiser, and the lead organiser for the IndieWebCamp Nürnberg event had a discussion about the extreme heat that was forecast for the event. This led to our creating a section on the event wiki page called “Heat Safety” which summarised the weather forecast (temperatures expected to range from 21C to 38C), the accommodations made by the venue, and general advice we had (bring a hat, suncream, sunglasses, etc.).</p><p>While the office where we had the event did not have air conditioning, the staff reported the temperature was relatively cool prior to the event. When the event happened, even with ~23 people in the office – which was incredibly spacious – the room remained at a manageable temperature. Thank you to Tollwerk for helping to ensure this was the case!</p><p>While this particular event was manageable in terms of temperature, I did note that we may want to re-think where we organise events in summer. The heatwave that happened could not have been predicted when the event was first scheduled months ago, but with an increased prevalence of heatwaves likely in summer, heat should definitely be something that event organisers, both in the IndieWeb community and in general, consider when scheduling events.</p><p>Admittedly, I was concerned about travelling somewhere that was 10C warmer than Scotland which, at the time, was experiencing the heatwave. I was already warm at home; going somewhere hotter felt daunting. I coped well in the heat in Nürnberg, avoiding the sunlight as much as I could at peak times, and taking other precautions to stay safe, but I did want to record how, even for someone who loves going to events, heat was a real concern.</p><p>On this note, a few years ago, I attended a Beyond Tellerrand event in Berlin in September. It was so warm the organiser moved the event to be over a month later the next year, to help avoid the same heat scenario for future events. This was the first thing that came to mind when I was voicing my concerns about organising events in summer; the IndieWeb community is far from alone in needing to consider accommodations for heat safety.</p><h2 id="reflections">Reflections</h2><p>IndieWebCamp events are among the highlights of my year. It is a delight to see people who make websites in person and to chat about all things that are on our minds. Because there were enough session ideas to have two event tracks, we all had to make choices about which sessions we attended. Thankfully, because notes are taken and sessions are recorded, we can all review the sessions we couldn’t attend afterward. I am planning to look through some of the notes in the coming days to both see what was discussed and see if any new ideas come to mind for things I can do on my website.</p><p>I am delighted by all the progress I made on both my website and Artemis, too. In particular, the logic <a href="https://jamesg.blog/2026/07/04/processing-feed-items-without-urls" rel="noreferrer">I wrote to process web feed entries without URLs</a> helped make Artemis significantly more robust, and allowed me to discover that a project I worked on a while ago that generated feeds whose entries did not have URLs was actually still working; it was just that Artemis couldn’t handle the feed because unique URLs were not generated.</p><p>The IndieWeb community lists all upcoming IndieWebCamp events on the <a href="https://events.indieweb.org/">IndieWeb Events</a> web page. We also have both in-person and online Homebrew Website Club events, which last ~90 minutes (depending on the meeting) where participants can discuss all things related to personal websites. I help host the Edinburgh in-person and Europe online meet-ups. If you’re interested in joining, you are more than welcome. Check the <a href="https://events.indieweb.org/">IndieWeb Events list</a> for a list of upcoming events, and don’t hesitate to <a href="https://jamesg.blog/email">email me</a> if you have questions.</p>
<!--kg-card-begin: html-->
<p><a class="u-syndication" href="https://news.indieweb.org/en">Also posted on IndieNews</a>.</p>
<!--kg-card-end: html--><script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a16500543f2a5e57',t:'MTc4MzI0MDE1OQ=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
<a class="tag" href="https://events.indieweb.org/">IndieWeb Events</a>
<a class="tag" href="https://events.indieweb.org/">IndieWeb Events list</a>
<a class="tag" href="https://indieweb.org/2026/Nuremberg">IndieWebCamp in Nürnberg, Germany</a>
<a class="tag" href="https://indieweb.org/2026/Nuremberg/Schedule">Notes for all of these sessions are archived on the IndieWeb wiki</a>
<a class="tag" href="https://indieweb.org/2026/Nuremberg/blogchat">Archiving live sessions / co-creating</a>
<a class="tag" href="https://indieweb.org/2026/Nuremberg/thenewweb">Will the way we consume websites permanently change?</a>
<a class="tag" href="https://indieweb.org/Create_Day">Create Day</a>
<a class="tag" href="https://indieweb.org/IndieWebCamp">IndieWebCamps</a>
<a class="tag" href="https://indieweb.org/what_to_make_at_IndieWebCamp">“what to make at IndieWebCamp” page on the wiki</a>
<a class="tag" href="https://jamesg.blog/2026/07/04/processing-feed-items-without-urls">Artemis can now process feed items that don’t have URLs</a>
<a class="tag" href="https://jamesg.blog/2026/07/04/processing-feed-items-without-urls">I wrote to process web feed entries without URLs</a>
<a class="tag" href="https://jamesg.blog/bookshelf">bookshelf</a>
<a class="tag" href="https://jamesg.blog/email">email me</a>
<a class="tag" href="https://jamesg.blog/indiewebcamp">IndieWebCamp category</a>
<a class="tag" href="https://jkphl.is/">Joschi</a>
<a class="tag" href="https://news.indieweb.org/en">Also posted on IndieNews</a>
<a class="tag" href="https://noracudazzo.com/">Nora</a>
<a class="tag" href="https://tantek.com/">Tantek</a>
<a class="tag" href="https://thedriftinghealer.com">Una</a>
<a class="tag" href="https://tollwerk.de/">Tollwerk</a>
<a class="tag" href="https://www.maxhaesslein.de/">Max</a>
Time-based background colour transitions with Temporal and CSS color-mix - localghosthttps://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/2026-07-05T00:00:00.000Z<p>I've given my website a bit of a refresh! There's a slightly updated layout if you're on desktop, plus I ditched the <code>etc</code> page and I've revamped my <a href="https://localghost.dev/links">links page</a> to be powered by <a href="https://raindrop.io">raindrop.io</a>. The <a href="https://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/">minimalist theme</a> is still minimalist, but a bit more fancy. The <a href="https://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/">vaporwave theme</a> has a newly jazzed-up nav bar with some adorable little icons. But the biggest change is to the <a href="https://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/">city theme</a>, which was previously a starry-sky dark mode theme.</p>
<p>If you're reading this between the hours of 9pm - 5am, you might be wondering what all the fuss is about - it looks pretty much the same as it did before. That's because the theme changes depending on the time of day!</p>
<div class="image-grid">
<picture><source type="image/webp" srcset="https://localghost.dev/img/BZAiQcUC0U-280.webp 280w"><img loading="lazy" decoding="async" src="https://localghost.dev/img/BZAiQcUC0U-280.jpeg" alt="A screenshot of the sunrise version of this layout, with pixel art skyscrapers at the bottom. The background is a blue to pink to light orange gradient" width="280" height="300"></picture><picture><source type="image/webp" srcset="https://localghost.dev/img/MeJRe8T7yk-280.webp 280w"><img loading="lazy" decoding="async" src="https://localghost.dev/img/MeJRe8T7yk-280.jpeg" alt="A screenshot of the daytime version of this layout, with pixel art skyscrapers at the bottom. The background is a purple to pink gradient" width="280" height="300"></picture><picture><source type="image/webp" srcset="https://localghost.dev/img/NFDu_9WIlE-280.webp 280w"><img loading="lazy" decoding="async" src="https://localghost.dev/img/NFDu_9WIlE-280.jpeg" alt="A screenshot of the sunset version of this layout, with pixel art skyscrapers at the bottom. The background is a purple to pink to orange gradient" width="280" height="300"></picture><picture><source type="image/webp" srcset="https://localghost.dev/img/6CcP_uABhY-280.webp 280w"><img loading="lazy" decoding="async" src="https://localghost.dev/img/6CcP_uABhY-280.jpeg" alt="A screenshot of the nighttime version of this layout, with pixel art skyscrapers at the bottom. There are pixel art stars in the header and the theme is now dark mode. The background is a dark blue to light blue to purple gradient" width="280" height="300"></picture>
</div>
<p>You can select the time of day using the picker in the top right, after the theme switcher. I'm persisting the choice in session storage so you don't get attacked by sudden light mode when changing pages, but if you visit again in the future it'll reset back to "now".</p>
<p>I was going to just turn the layout into a pastel lo-fi-aesthetic thing, but then I realised that a) I needed <em>some</em> kind of dark mode and b) I'd miss the stars! So I thought... why not both? And why stop at just night and day? (Hat tip to <a href="https://alistairshepherd.uk/">Alistair Shepherd</a> who did something similar with his beautiful Firewatch-inspired website.)</p>
<p>Then I remembered that the Temporal API was available experimentally in Chrome and Firefox, and I'd been looking for an excuse to try it out.</p>
<h2 id="introducing-temporal" tabindex="-1">Introducing Temporal</h2>
<p>For the uninitiated, Temporal is a solution to the objectively terrible Date API in JavaScript. Date was based on Java's Date library, which was also objectively terrible and has long been deprecated.</p>
<p>It's always really confusing that <code>Date</code> instances show either local or UTC time depending on which function you use to display them, and date operations are so fiddly that most of us turn to third party libraries like <code>date-fns</code> or <code>luxon</code>.</p>
<p>Temporal massively simplifies the API, introducing some new concepts:</p>
<ul>
<li><code>PlainDateTime</code>: a date and time with no timezone (TZ)</li>
<li><code>PlainDate</code>: a date with no time information and no TZ</li>
<li><code>PlainTime</code>: a time with no date information and no TZ</li>
<li><code>ZonedDateTime</code>: a date and time in a specified TZ</li>
</ul>
<p><code>PlainTime</code> came in useful for this project, as we don't really care what the day is - only what time it is, so we know what colours to show.</p>
<h3 id="getting-the-users-local-time" tabindex="-1">Getting the user's local time</h3>
<p>The first thing to do was figure out the time according to the user's browser.<br>
The <code>Temporal.Now</code> <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now">namespace</a> has various methods for interacting with the current time, including <code>plainTimeISO()</code> which by default gives us a <code>PlainTime</code> in local time. (You can also pass in a time zone to get a zoned time.)</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> timeNow <span class="token operator">=</span> Temporal<span class="token punctuation">.</span>Now<span class="token punctuation">.</span><span class="token function">plainTimeISO</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Now we need to know when to show the different colours.</p>
<h2 id="defining-the-stages" tabindex="-1">Defining the stages</h2>
<p>The day is split into four stages: sunrise, daytime, sunset and night. Daytime and night are long - 11.5 hours each - whereas sunrise and sunset each last 90 minutes.</p>
<p>The background of the page has a two-colour gradient:</p>
<pre class="language-css"><code class="language-css"> <span class="token property">--background</span><span class="token punctuation">:</span> fixed <span class="token function">linear-gradient</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-top<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-mid<span class="token punctuation">)</span> 80%<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>The footer has an additional colour that's created with a linear gradient from transparent <code>oklch(0 0 0 / 0)</code> to the chosen third colour.</p>
<pre class="language-css"><code class="language-css"> <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">linear-gradient</span><span class="token punctuation">(</span><span class="token function">oklch</span><span class="token punctuation">(</span>0 0 0 / 0<span class="token punctuation">)</span> 40%<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-bottom<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>This means the last colour sticks to the bottom of the page rather than stretching across the viewport height (it's hard to control even when you specify a percentage in the gradient). It also gives more of a glow that really looks like the sun rising/setting or the glow of the city, which I love.</p>
<p>I defined an object for the stages and colours:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> stages <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token literal-property property">sunrise</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">start</span><span class="token operator">:</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"06:30:00"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">next</span><span class="token operator">:</span> <span class="token string">"day"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color1</span><span class="token operator">:</span> <span class="token string">"oklch(0.618 0.3157 265.76)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color2</span><span class="token operator">:</span> <span class="token string">"oklch(0.8867 0.1222 328.24)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color3</span><span class="token operator">:</span> <span class="token string">"oklch(0.9529 0.1222 106.94)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">day</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">start</span><span class="token operator">:</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"08:00:00"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">next</span><span class="token operator">:</span> <span class="token string">"sunset"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color1</span><span class="token operator">:</span> <span class="token string">"oklch(58% 0.15433 300)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color2</span><span class="token operator">:</span> <span class="token string">"oklch(85% 0.22133 302)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color3</span><span class="token operator">:</span> <span class="token string">"oklch(98 0.22133 302)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">sunset</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">start</span><span class="token operator">:</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"19:30:00"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">next</span><span class="token operator">:</span> <span class="token string">"night"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color1</span><span class="token operator">:</span> <span class="token string">"oklch(0.6933 0.1899 297.53)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color2</span><span class="token operator">:</span> <span class="token string">"oklch(75.504% 0.24612 357.26)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color3</span><span class="token operator">:</span> <span class="token string">"oklch(88.591% 0.1422 62.595)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">night</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">start</span><span class="token operator">:</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"21:00:00"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">next</span><span class="token operator">:</span> <span class="token string">"sunrise"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color1</span><span class="token operator">:</span> <span class="token string">"oklch(25.27% 0.0919 276.73)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color2</span><span class="token operator">:</span> <span class="token string">"oklch(47.35% 0.284 283.78)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color3</span><span class="token operator">:</span> <span class="token string">"oklch(62.831% 0.23521 310.291)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>CSS custom properties are easy to set via JS - you can use <code>root.style.setProperty</code>:</p>
<pre class="language-js"><code class="language-js"> root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span>
<span class="token string">"--bg-gradient-top"</span><span class="token punctuation">,</span>
<span class="token string">"oklch(25.27% 0.0919 276.73)"</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Unlike <code>Date</code>, we don't have to do any gymnastics to compare Temporal instances: there's literally a <code>compare</code> function on each type of instance. Just like with other JS comparison functions, it returns <code>1</code> if the first instance is greater than the second, <code>0</code> if the two instances are the same, and <code>-1</code> if the first instance is less than the second.</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">const</span> compare <span class="token operator">=</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span>compare <span class="token comment">// extracted for brevity</span>
<span class="token keyword">switch</span> <span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunrise<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span> <span class="token operator">||</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"night"</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunrise<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>day<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"sunrise"</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>day<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunset<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"day"</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunset<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"sunset"</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">default</span><span class="token operator">:</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Once we've got the stage name, we can look up the colours and set the custom property values.</p>
<pre class="language-js"><code class="language-js">
root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span>
<span class="token string">"--bg-gradient-top"</span><span class="token punctuation">,</span>
stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color1<span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span>
<span class="token string">"--bg-gradient-mid"</span><span class="token punctuation">,</span>
stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color2<span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span>
<span class="token string">"--bg-gradient-bottom"</span><span class="token punctuation">,</span>
stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color3<span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>I'm also setting a data attribute on the root so we can do some additional stage-based customisations, such as showing the stars when it's night.</p>
<pre class="language-js"><code class="language-js">root<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">"data-time"</span><span class="token punctuation">,</span> currentStageName<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And that will give us our different gradient colours at different times of day!</p>
<p>And <em>then</em> I remembered that <code>color-mix</code> exists. Why restrict ourselves to just 4 times of day and 4 sets of colours, when we could make them... transition into each other?????</p>
<h2 id="blending-transitions-with-color-mix" tabindex="-1">Blending transitions with color-mix</h2>
<p><code>color-mix</code> is an extremely cool CSS function that lets you, well, mix two colours together. You tell it what colour space you're working with, and the colours, and the browser magically outputs the mix between the two.</p>
<pre class="language-css"><code class="language-css"><span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">color-mix</span><span class="token punctuation">(</span>in oklch<span class="token punctuation">,</span> color1<span class="token punctuation">,</span> color2<span class="token punctuation">)</span></code></pre>
<p>Much like with gradients, you can also specify a percentage value for the colours, which indicates the proportions of the colours:</p>
<pre class="language-css"><code class="language-css"><span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">color-mix</span><span class="token punctuation">(</span>in oklch<span class="token punctuation">,</span> color1 20%<span class="token punctuation">,</span> color2<span class="token punctuation">)</span></code></pre>
<p>So I could gradually feed in a bit of the next stage's colour until the next stage took over completely.</p>
<p>To get a percentage value for the next stage colour to feed in, I had to figure out how far through the current stage we are.</p>
<p>First, I'm calculating the time until the next stage - super simple with the <code>until</code> function on <code>Temporal</code> instances:</p>
<pre class="language-js"><code class="language-js">time1<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>time2<span class="token punctuation">)</span></code></pre>
<p>This gives us a <code>Temporal.Duration</code> which represents a period between two time points. So, for example, if it's 7:45pm now and we're calculating <code>timeUntilNextStage</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> timeUntilNextStage <span class="token operator">=</span> timeNow<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>timeUntilNextStage<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment">// PT1H15M</span>
</code></pre>
<p><code>Duration</code>s are stringified (and specified) using the <a href="https://en.wikipedia.org/wiki/ISO_8601#Durations">ISO 8601 duration format</a>, so "PT1H15M" means "period, time separator, 1 hour, 15 minutes".Time information appears after the <code>T</code>; if the duration had any date information in it, it'd appear before the <code>T</code>.</p>
<p>We set <code>timeUntilNextStage</code> in the switch statement where we're deciding what stage we're in, for example:</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunrise<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>day<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"sunrise"</span><span class="token punctuation">;</span>
timeUntilNextStage <span class="token operator">=</span> timeNow<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Once we've got the duration representing time until the next stage, we need to know the duration between the start of the current stage and the start of the next stage - let's call it the "transition duration". For sunset-to-night and sunrise-to-day, the transition duration is always 90 minutes; for night-sunrise and day-sunset, it'd be 11.5 hours. I didn't want the colour mixing to happen all throughout the day, only around sunrise/sunset like in real life, so I just decided to hardcode the transition duration for day and night to be 90 minutes so it matches the other two.</p>
<p>So for that, I can instantiate a <code>Duration</code> using the same ISO 8601 syntax:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> entireTransitionDuration <span class="token operator">=</span> Temporal<span class="token punctuation">.</span>Duration<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"PT1H30M"</span><span class="token punctuation">)</span></code></pre>
<p>Now I need to calculate the difference between the total duration and the time until next stage - basically, how far into the transition period we are, and therefore how much of a percentage we should mix in of the next colour.</p>
<p>Handily, Temporal gives us a <code>subtract</code> function as well:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> diff <span class="token operator">=</span> entireTransitionDuration<span class="token punctuation">.</span><span class="token function">subtract</span><span class="token punctuation">(</span>timeUntilNextStage<span class="token punctuation">)</span></code></pre>
<p>Then to figure out the transition progress as a percentage, we can divide <code>diff</code> by <code>entireTransitionDuration</code>. We'll do that with the time values in seconds so we can divide them, using the instance's <code>total</code> function:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> entireTransitionDurationInSeconds <span class="token operator">=</span> entireTransitionDuration<span class="token punctuation">.</span><span class="token function">total</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">unit</span><span class="token operator">:</span> <span class="token string">"seconds"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> diffInSeconds <span class="token operator">=</span> diff<span class="token punctuation">.</span><span class="token function">total</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">unit</span><span class="token operator">:</span> <span class="token string">"seconds"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> transitionProgressPercent <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span><span class="token punctuation">(</span>diffInSeconds <span class="token operator">/</span> entireTransitionDurationInSeconds<span class="token punctuation">)</span><span class="token operator">*</span><span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// gives us a string representation with 0 d.p.</span></code></pre>
<h3 id="the-midnight-problem" tabindex="-1">The midnight problem</h3>
<p>It's a little more complicated for the "night" stage, because that crosses midnight into the next day. Remember that our <code>PlainTime</code> only has time information, not date information - so if it's 10pm and you're asking it how long until sunrise at 6:30am, it'll give you a negative number!</p>
<pre class="language-js"><code class="language-js">
<span class="token keyword">const</span> now <span class="token operator">=</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"22:00"</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> sunrise <span class="token operator">=</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"06:30"</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> d <span class="token operator">=</span> now<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>sunrise<span class="token punctuation">)</span> <span class="token comment">// Temporal.Duration -PT15H30M</span></code></pre>
<p>This causes problems at the point where I calculate the diff, as it'll come out as a large number and completely throw off the calculations. I got around this by getting the absolute value of the duration with <code>.abs()</code>, so <code>timeUntilNextStage</code> will always be positive, even if it's before midnight: e.g. what was<code>-PT15H30M</code> will now be <code>PT15H30M</code>. Calculating the diff by subtracting that from a <code>transitionDuration</code> of 90 mins will always yield a negative number.</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunrise<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span> <span class="token operator">||</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"night"</span><span class="token punctuation">;</span>
timeUntilNextStage <span class="token operator">=</span> timeNow<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>stages<span class="token punctuation">.</span>sunrise<span class="token punctuation">.</span>start<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">abs</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Then, we only calculate a transition percentage if <code>diff</code> is greater than 0:</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">let</span> transitionProgressPercent <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>diffInSeconds <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
transitionProgressPercent <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span><span class="token punctuation">(</span>diffInSeconds <span class="token operator">/</span> entireTransitionDurationInSeconds<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>This works for the daytime stage too: if it's more than 90 mins before sunset, it'll come out with a negative diff - so that will just display the daytime colours and no transition.</p>
<h3 id="lets-mix" tabindex="-1">Let's mix!</h3>
<p>Now we can use that percentage value (which will always be a whole number) in the <code>color-mix</code> function to dictate how much of the next colour we should interpolate.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">color-mix(in oklch, $</span><span class="token punctuation">{</span>color1<span class="token punctuation">}</span> <span class="token selector">$</span><span class="token punctuation">{</span>transitionProgressPercent<span class="token punctuation">}</span><span class="token selector">%, $</span><span class="token punctuation">{</span>color2<span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>I updated my <code>stages</code> object to include the next stage name as well:</p>
<pre class="language-js"><code class="language-js"> <span class="token literal-property property">night</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">start</span><span class="token operator">:</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"21:00:00"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">next</span><span class="token operator">:</span> <span class="token string">"sunrise"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color1</span><span class="token operator">:</span> <span class="token string">"oklch(25.27% 0.0919 276.73)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color2</span><span class="token operator">:</span> <span class="token string">"oklch(47.35% 0.284 283.78)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color3</span><span class="token operator">:</span> <span class="token string">"oklch(62.831% 0.23521 310.291)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token comment">// etc</span></code></pre>
<p>So we can get both colours dynamically when we set the variables with <code>color-mix</code>:</p>
<pre class="language-js"><code class="language-js"> root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span>
<span class="token string">"--bg-gradient-top"</span><span class="token punctuation">,</span>
<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">color-mix(in oklch, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>stages<span class="token punctuation">[</span>nextStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color1<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>transitionProgressPercent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color1<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">)</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And that's how we transition the colours!</p>
<h2 id="transitioning-the-transitions" tabindex="-1">Transitioning the transitions</h2>
<p>As a bonus touch, I wanted the colour change to transition smoothly when you switch between stages manually using the picker on the top right. By declaring my <code>bg-gradient-xx</code> variables using <code>@property</code>, I can tell the browsers that yes, they are definitely colours - and therefore they can be animated.</p>
<p>Without this explicit custom property declaration, I could set the value of <code>--bg-gradient-top</code> to a number, or a position, or anything I wanted. By saying it's definitely a colour, the browser knows how to transition it into other values of the same type.</p>
<p>I initially did this with <code>@property</code> declarations in the CSS:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@property</span> --bg-gradient-top</span> <span class="token punctuation">{</span>
<span class="token property">syntax</span><span class="token punctuation">:</span> <span class="token string">"<color>"</span><span class="token punctuation">;</span>
<span class="token property">inherits</span><span class="token punctuation">:</span> true<span class="token punctuation">;</span>
<span class="token property">initial-value</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>...<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token atrule"><span class="token rule">@property</span> --bg-gradient-mid</span> <span class="token punctuation">{</span>
<span class="token property">syntax</span><span class="token punctuation">:</span> <span class="token string">"<color>"</span><span class="token punctuation">;</span>
<span class="token property">inherits</span><span class="token punctuation">:</span> true<span class="token punctuation">;</span>
<span class="token property">initial-value</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>...<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token atrule"><span class="token rule">@property</span> --bg-gradient-bottom</span> <span class="token punctuation">{</span>
<span class="token property">syntax</span><span class="token punctuation">:</span> <span class="token string">"<color>"</span><span class="token punctuation">;</span>
<span class="token property">inherits</span><span class="token punctuation">:</span> true<span class="token punctuation">;</span>
<span class="token property">initial-value</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>...<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Unfortunately, setting these in the CSS meant that you got a flash of whichever initial values I'd set before the JS kicked in and set the appropriate colours for time of day. If this page were server-driven, or always started from the same colour for everyone, it would've been fine. But the starting colour depends on your time zone and is only calculated when the initial JS runs.</p>
<p>I got around this by setting the properties via JS instead:</p>
<pre class="language-js"><code class="language-js"> window<span class="token punctuation">.</span><span class="token constant">CSS</span><span class="token punctuation">.</span><span class="token function">registerProperty</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"--bg-gradient-top"</span><span class="token punctuation">,</span>
<span class="token literal-property property">syntax</span><span class="token operator">:</span> <span class="token string">"<color>"</span><span class="token punctuation">,</span>
<span class="token literal-property property">inherits</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token literal-property property">initialValue</span><span class="token operator">:</span> stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color1<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span><span class="token constant">CSS</span><span class="token punctuation">.</span><span class="token function">registerProperty</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"--bg-gradient-mid"</span><span class="token punctuation">,</span>
<span class="token literal-property property">syntax</span><span class="token operator">:</span> <span class="token string">"<color>"</span><span class="token punctuation">,</span>
<span class="token literal-property property">inherits</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token literal-property property">initialValue</span><span class="token operator">:</span> stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color2<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span><span class="token constant">CSS</span><span class="token punctuation">.</span><span class="token function">registerProperty</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"--bg-gradient-bottom"</span><span class="token punctuation">,</span>
<span class="token literal-property property">syntax</span><span class="token operator">:</span> <span class="token string">"<color>"</span><span class="token punctuation">,</span>
<span class="token literal-property property">inherits</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token literal-property property">initialValue</span><span class="token operator">:</span> stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color3<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>I had to wrap these in a <code>try/catch</code> as it will throw if the property's already been defined. It wasn't super trivial to figure out if this property had already been set, as the CSS does define some values for these with the regular <code>--bg-gradient-xx: ...</code> syntax.</p>
<p>On the <code>body</code> and <code>footer</code> I set <code>transition-property</code> and <code>transition-duration</code> to tell it which properties I want to animate:</p>
<pre class="language-css"><code class="language-css"> <span class="token selector">body</span> <span class="token punctuation">{</span>
<span class="token property">--background</span><span class="token punctuation">:</span> fixed <span class="token function">linear-gradient</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-top<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-mid<span class="token punctuation">)</span> 80%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">transition-property</span><span class="token punctuation">:</span> --bg-gradient-top<span class="token punctuation">,</span> --bg-gradient-mid<span class="token punctuation">;</span>
<span class="token property">transition-duration</span><span class="token punctuation">:</span> 0.5s<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">footer</span> <span class="token punctuation">{</span>
<span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">linear-gradient</span><span class="token punctuation">(</span><span class="token function">oklch</span><span class="token punctuation">(</span>0 0 0 / 0<span class="token punctuation">)</span> 40%<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-bottom<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">transition</span><span class="token punctuation">:</span> --bg-gradient-bottom 0.5s<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>And like motherflipping magic, the colours transition seamlessly into each other when the values change! I love CSS. The animation is such an unnecessary touch, but this is my website so unnecessary is the name of the game.</p>
<h2 id="polyfilling-temporal-for-safari" tabindex="-1">Polyfilling Temporal for Safari</h2>
<p>Alas, Safari is behind the times. We love progressive enhancement, and of course I could have just removed any of the transition logic for people whose browsers don't support Temporal, but that's no fun. They deserve sunsets too!</p>
<p>Writing a shim for Temporal was also no fun, but I did it because I love you.</p>
<p>There are various Temporal polyfills around and about, but I didn't want to end up importing a whole lot of extra JS when I only needed one or two functions. I'm not using any kind of bundler on this site - I use Eleventy to generate the pages, but scripts are just imported vanilla - so I couldn't import something with NPM and expect it to tree-shake any bits I wasn't using. It was a lot more lightweight to just write my own.</p>
<p>To check for Temporal support, it's a matter of just checking if <code>window.Temporal?.PlainTime</code> is undefined:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> supportsTemporal <span class="token operator">=</span> <span class="token keyword">typeof</span> window<span class="token punctuation">.</span>Temporal<span class="token operator">?.</span>PlainTime <span class="token operator">!==</span> <span class="token string">"undefined"</span><span class="token punctuation">;</span></code></pre>
<p>I'm checking for <code>PlainTime</code> specifically as some browsers may have very high level Temporal implementations, but we can't do much without <code>PlainTime</code>.</p>
<p>To get the user's time in a non-Temporal world, we can just call the good old-fashioned <code>new Date()</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">getUserTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>supportsTemporal<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> Temporal<span class="token punctuation">.</span>Now<span class="token punctuation">.</span><span class="token function">plainTimeISO</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>To compare dates, we do it by comparing epoch timestamps. These represent the number of milliseconds since the Unix epoch, 01 Jan 1970.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">jsDateCompare</span><span class="token punctuation">(</span><span class="token parameter">date1<span class="token punctuation">,</span> date2</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> date1Ms <span class="token operator">=</span> date1<span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> date2Ms <span class="token operator">=</span> date2<span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>date1Ms <span class="token operator">===</span> date2Ms<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> date1Ms <span class="token operator"><</span> date2Ms <span class="token operator">?</span> <span class="token operator">-</span><span class="token number">1</span> <span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Then, we can just assign whichever version of the function we need:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> compare <span class="token operator">=</span> supportsTemporal <span class="token operator">?</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span>compare <span class="token operator">:</span> jsDateCompare<span class="token punctuation">;</span></code></pre>
<p>To polyfill <code>until</code>, I've got a <code>durationBetween</code> function which will call <code>until</code> if Temporal's supported, otherwise it'll subtract two epoch timestamps, and divide the result by 1000 to get the duration as seconds:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">durationBetween</span><span class="token punctuation">(</span><span class="token parameter">time1<span class="token punctuation">,</span> time2</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>supportsTemporal<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token punctuation">(</span>time2<span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> time1<span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">/</span> <span class="token number">1000</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> time1<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>time2<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Then I call it like this:</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunset<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"sunset"</span><span class="token punctuation">;</span>
timeUntilNextStage <span class="token operator">=</span> <span class="token function">durationBetween</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>I wrote a whole suite of unit tests (for a PERSONAL project! I know!) to make sure behaviour was exactly the same, and it seems to be working nicely. I'm hoping I can remove the polyfills in time, but given that the web is beautifully backwards-compatible, it's not the end of the world if it stays around longer than it needs to.</p>
<h2 id="fixing-a-weird-background-glitch-in-safari" tabindex="-1">Fixing a weird background glitch in Safari</h2>
<p><picture><source type="image/webp" srcset="https://localghost.dev/img/cmqGHiVsJL-280.webp 280w, https://localghost.dev/img/cmqGHiVsJL-640.webp 640w, https://localghost.dev/img/cmqGHiVsJL-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/cmqGHiVsJL-280.jpeg" alt="A screenshot of the bottom half of my website, with a big white space in the background where the background should be. The background gradient cuts off a third of the way down the screen." width="960" height="616" srcset="https://localghost.dev/img/cmqGHiVsJL-280.jpeg 280w, https://localghost.dev/img/cmqGHiVsJL-640.jpeg 640w, https://localghost.dev/img/cmqGHiVsJL-960.jpeg 960w" sizes="auto"></picture><br>
SAFARI WHY.</p>
<p>I started experiencing a very odd glitch in Safari for MacOS where the background gradient would only show up in the initial viewport - when you scrolled, it went white or black depending on whether it was light or dark mode. I narrowed it down to the <code>background-attachment: fixed</code> property of the body.</p>
<p><video controls="" loading="lazy"><source src="https://localghost.dev/img/background-glitch-safari-demo.mp4" type="video/mp4"></video></p>
<p>After a lot of disabling random CSS and diffing against the <code>main</code> branch, which doesn't have that problem, I found out that it really doesn't play nicely with <code>container-type: inline-size</code>. In the course of redesigning the site, I'd added a new container context to the <code><body></code> element and in the process broken the gradient rendering for Safari, as something goes wrong when it tries to render the gradient background with a <code>fixed</code> attachment. Chrome, Firefox and iOS Safari were totally fine.</p>
<p>When I half-jokingly said I'd have to find out what the modern equivalent of <code><!--[IF IE]></code> was, David Bushell <a href="https://social.lol/@db/116867834799255143">pointed me to</a> Eric Meyer's <a href="https://meyerweb.com/eric/thoughts/2026/05/28/accessible-i-think-split-cell-table-headers/#:~:text=@supports%20(font:%20-apple-system-body)">post about accessible table headers</a>, which in turn led me to <a href="https://browserstrangeness.github.io/css_hacks.html#safari">Browser Strangeness</a>. That did indeed have a <code>@supports</code> query that targeted Safari for MacOS:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@supports</span> <span class="token punctuation">(</span><span class="token keyword">not</span> <span class="token punctuation">(</span><span class="token property">-webkit-text-size-adjust</span><span class="token punctuation">:</span>none<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">and</span> <span class="token punctuation">(</span><span class="token property">font</span><span class="token punctuation">:</span> -apple-system-body<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">.selector</span> <span class="token punctuation">{</span> <span class="token property">property</span><span class="token punctuation">:</span>value<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre>
<p>I stuck a <code>position-attachment: initial</code> in there, and lo and behold, the problem went away. It means the background isn't quite how I wanted it to look in Safari, but I'll survive.</p>
<h2 id="this-was-surprisingly-complex" tabindex="-1">This was surprisingly complex</h2>
<p>The individual moving parts of this project - getting the time and choosing colours, mixing the colours by percentages, animating the transitions - were not that complicated in isolation. Sure, they required me to learn things and look things up, but it was a fun thing to build (until the point where Safari came into the picture).</p>
<p>The challenge came wiring it all together in a way that didn't cause flashes of unstyled content (the dreaded FOUC) or flashes of the wrong stage before we calculate the current time. This is a static site, so it's all client-side JS. Ideally I'd compute user's the current time on the server and serve the content with the correct colour values in the HTML, but my web host only supports static sites.</p>
<p>To get around that I had to add another separate <code>init.js</code> script which runs instantly - it's got a bit of a copy and paste job going on with some of the functions, but it does a very rudimentary check of the user's current time and sets the stage accordingly with no transitions, just so there's <em>some</em> styling on initial load. My JS is all modules, so is <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#defer">deferred by default</a>. I experimented with making <em>all</em> the JS render-blocking with <code>blocking="render"</code>, but that felt a bit gross and also didn't fix the FOUC in Firefox.</p>
<p>But that's fine, y'know? It still loads in well under a second, and still looks good if you have JS disabled. It's my personal site and it doesn't need to be perfect.</p>