<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Krizdingus</title><description>Posts from krizdingus.com: how the site is built, and the stories behind a few of the projects.</description><link>https://krizdingus.com/</link><language>en</language><atom:link href="https://krizdingus.com/rss.xml" rel="self" type="application/rss+xml"/><item><title>A theme is a folder</title><link>https://krizdingus.com/blog/a-theme-is-a-folder/</link><guid isPermaLink="true">https://krizdingus.com/blog/a-theme-is-a-folder/</guid><description>This site has a second look, an old-computer desktop called XP mode, and you only get it if you ask. Here&apos;s how it&apos;s built so the normal pages never have to carry it.</description><pubDate>Tue, 20 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you’re on a laptop or a desktop computer right now, there’s a little arrow up in the header, right next to the light and dark button. Open it and there’s an entry that says XP mode, with a tiny pixel drawing of an old CRT monitor next to it. Pick it and the page you’re reading switches off like an old tube TV and comes back as the desktop of a computer that doesn’t exist. Same post, but now it’s sitting inside a window, with icons, a taskbar and a Start menu around it. The first time you go in there’s a boot screen too, which you can skip (and which never plays if your computer is set to cut down on motion). You can also just type the word “boot” anywhere on the site, as long as you’re not in a text box, and you’ll end up in the same place.&lt;/p&gt;
&lt;p&gt;The XP is for Windows XP, roughly. That’s when I was using the computer the most. I know ’95 and ’98 too, but XP is when the internet really took off and most people were online regularly. It’s not trying to be an exact copy of anything. For nerd and web-culture people, Windows XP is like the shag carpet of the ’70s.&lt;/p&gt;
&lt;p&gt;If you’re on a phone you don’t get that entry at all. That part’s deliberate.&lt;/p&gt;
&lt;h2 id=&quot;two-looks-one-site&quot;&gt;Two looks, one site&lt;/h2&gt;
&lt;p&gt;On a lot of sites a theme switch just means light or dark. This site has that too, in the same little menu: Auto, which just goes along with whatever your device is set to, plus Light and Dark. It works in either look. In WordPress a theme is the whole design, but somebody picks it once, on the server, and every visitor gets the same one. This is closer to the WordPress kind, except you’re the one picking it, and it happens right there in your browser without loading a new page. It takes what’s already on the page and rearranges it.&lt;/p&gt;
&lt;p&gt;For most of this rebuild the desktop was the site, and the plain version was a setting in the Start menu. There was even a popup asking if it was all too much. The popup’s gone. Normal mode is the default now: a plain header, the page and a footer, and it works with JavaScript turned off. You only get the desktop if you ask for it, or if you open one of the little app pages on a computer, which I’ll get to.&lt;/p&gt;
&lt;p&gt;There’s only one theme, and I don’t have a second one planned. I still built it so another one could drop in next to it without touching anything else. I wanted it to work like a real feature, lightweight and accessible. Some of it is also just showing off, and this post is kind of that part. It gets more technical from here.&lt;/p&gt;
&lt;h2 id=&quot;the-manifest&quot;&gt;The manifest&lt;/h2&gt;
&lt;p&gt;Each theme is a folder, &lt;code&gt;src/themes/&amp;lt;id&amp;gt;/&lt;/code&gt;, and one of the two files it has to have is a manifest. That’s just a small file of facts about the theme: what it’s called, a one-line description, where it’s allowed to run. This is the top of the one for XP mode:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; manifest&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ThemeManifest&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  id: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;xp&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  name: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;XP mode&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  description: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;The same site as a fake old-PC desktop: windows, a taskbar, a Start menu and a boot screen.&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Desktop only: a wide viewport and a mouse or trackpad. Never phones.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  availability: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;(min-width: 1024px) and (pointer: fine)&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rest of it covers the menu entry’s text and its little pixel-CRT icon, the keyframes for the swap, and a word you can type on the normal site to get in. That’s where “boot” comes from. There’s also &lt;code&gt;appRoutes: true&lt;/code&gt;, which says the app pages under &lt;code&gt;/apps/&lt;/code&gt;, like the MediaKruzer video player, open in this theme. On a computer that can run XP mode, those pages come up as the desktop no matter what you picked. Anywhere else they show a short note about what the app needs.&lt;/p&gt;
&lt;p&gt;The manifest has to stay pure data, just values and no code, because the build pastes pieces of it into the head of every page. The head is the part of the HTML the browser reads before it shows you anything. The site is built with Astro, which turns every page into a plain HTML file ahead of time, so “every page” really does mean every file.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;availability&lt;/code&gt; is a media query, which is a question the page asks the browser about the screen it’s on. This one asks two things: is the window at least 1024 pixels wide, and is there a mouse or a trackpad? A phone’s touchscreen doesn’t count, so phones never pass. Phones getting the plain site was already on my list back in May, before any of this was a theme, and that part never changed.&lt;/p&gt;
&lt;p&gt;Anywhere the query doesn’t match, you can’t use the switch. Shrink the window under 1024 pixels while you’re in the desktop and the runtime, the small script that runs the whole theme system, drops you back to the normal site. It doesn’t save that as your choice, since you didn’t really choose anything. Widen the window again and the desktop comes back.&lt;/p&gt;
&lt;h2 id=&quot;finding-themes&quot;&gt;Finding themes&lt;/h2&gt;
&lt;p&gt;Nothing keeps a list of themes. The theme system just globs for them, which means it looks for every file that matches a pattern, here any &lt;code&gt;&amp;lt;id&amp;gt;.theme.ts&lt;/code&gt; one folder down:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; themeEntries&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ThemeEntry&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; pairThemes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  themeManifests,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;meta&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;glob&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ThemeModule&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;./*/*.theme.ts&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That glob is lazy. Vite, the tool Astro uses to bundle up the JavaScript, builds each &lt;code&gt;&amp;lt;id&amp;gt;.theme.ts&lt;/code&gt; as its own chunk, a separate JavaScript file, and the browser only fetches it when that theme mounts, which is the word for a theme actually getting switched on. Normal pages never download the desktop. The only place it loads without you asking for it is those &lt;code&gt;/apps/&lt;/code&gt; pages.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;themeManifests&lt;/code&gt; comes from a second glob that isn’t lazy, since manifests are small and the head script needs them before anything else runs.&lt;/p&gt;
&lt;h2 id=&quot;the-contract&quot;&gt;The contract&lt;/h2&gt;
&lt;p&gt;Every theme hands the runtime the same few things, so the runtime never has to know anything about a particular theme. A theme module exports this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ThemeModule&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  /** Stylesheet URLs (import them with `?url`). The runtime adds them before&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;   *  mount() and removes them after unmount(), so theme CSS needs no scoping. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;  styles&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  /** Transform the page. Called after styles have loaded and after&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;   *  &amp;lt;html data-skin=&quot;&amp;lt;id&amp;gt;&quot;&amp;gt; is set. Must not await long work (the view&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;   *  transition holds the screen until it returns). */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  mount&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ThemeContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  /** Put every node you moved back where it was. Listeners on ctx.signal and&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;   *  ctx.onCleanup callbacks are handled for you. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  unmount&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ThemeContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s pretty much the whole deal. Mount switches the theme on, unmount switches it off. Theme CSS can be as heavy-handed as it likes, since it’s only on the page while the theme is mounted. The context handed to &lt;code&gt;mount()&lt;/code&gt; carries an &lt;code&gt;AbortSignal&lt;/code&gt;, which is basically a kill switch: it gets pulled right before &lt;code&gt;unmount()&lt;/code&gt;, and any event listener the theme attached with it removes itself. Anything else that needs tearing down goes to &lt;code&gt;onCleanup()&lt;/code&gt;, and the runtime cleans up after the theme.&lt;/p&gt;
&lt;p&gt;The XP theme moves the page’s &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; into an OS window and builds a desktop, taskbar and Start menu around it. Its &lt;code&gt;unmount()&lt;/code&gt; puts &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; back where it found it. It moves the real thing instead of making a copy, so the page’s scripts, where your keyboard focus was and anything you’d typed into a form all survive the trip, both ways.&lt;/p&gt;
&lt;h2 id=&quot;before-first-paint&quot;&gt;Before first paint&lt;/h2&gt;
&lt;p&gt;If you left the site in XP mode, the next page has to know that before it draws anything. Otherwise you’d get the normal site for a beat and then watch it turn into a desktop, which looks broken. So there’s a small script in the head that runs before the page draws, and the browser waits for it. It reads your stored choice, checks it against the theme’s media query, and marks the page as pending. Then this bit of CSS keeps the page hidden until the theme mounts:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;:root&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;data-skin-pending&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;:not&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;([&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;data-skin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]) &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;:not&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;([&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;data-motion-ok&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  visibility&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;hidden&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  animation&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: skin-failsafe &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;s&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; linear&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;s&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; forwards&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; !important&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@keyframes&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt; skin-failsafe&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;to&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;visibility&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;visible&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; } }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That animation is a timer. It lasts zero seconds and starts after four, so if the theme never arrives, because you’re offline or something blocked the script, the page shows up anyway. With JavaScript off the head script never runs, nothing is ever pending, and you get the plain normal site.&lt;/p&gt;
&lt;h2 id=&quot;the-swap&quot;&gt;The swap&lt;/h2&gt;
&lt;p&gt;The runtime fetches the theme module and its stylesheets before it touches the page. Each stylesheet goes in with &lt;code&gt;link.media = &apos;not all&apos;&lt;/code&gt;. A stylesheet whose media query matches nothing still downloads. It just doesn’t apply. At mount the runtime flips it to &lt;code&gt;all&lt;/code&gt;, so the swap itself is instant, and all the network stuff happens while the old page is still up.&lt;/p&gt;
&lt;p&gt;If your browser has View Transitions, a newer feature where the browser snapshots the old page and lets you animate from it to the new one, the swap runs inside one, and the manifest supplies the keyframes. For XP the old page collapses like a CRT powering off in 240 milliseconds and the new one powers on in 320. You can tell your computer you want less motion on screen (&lt;code&gt;prefers-reduced-motion&lt;/code&gt;, most operating systems have a setting for it), and then there’s no transition and the page just changes. Switches queue up and run one at a time, so mashing the button can’t start two swaps at once.&lt;/p&gt;
&lt;p&gt;The choice goes in &lt;code&gt;localStorage&lt;/code&gt;, the little box of settings your browser keeps for each site, under &lt;code&gt;krizdingus-skin&lt;/code&gt;. Yeah, skin. &lt;code&gt;krizdingus-theme&lt;/code&gt; was already the light and dark setting. Nothing stored means normal mode, except on the &lt;code&gt;/apps/&lt;/code&gt; pages, where the route wins. If storage is blocked, the theme lasts until you leave the page. Other open tabs listen for that key, so switching in one tab switches all of them.&lt;/p&gt;
&lt;h2 id=&quot;theme-2&quot;&gt;Theme #2&lt;/h2&gt;
&lt;p&gt;A second theme would need a folder with a &lt;code&gt;manifest.ts&lt;/code&gt; and an &lt;code&gt;&amp;lt;id&amp;gt;.theme.ts&lt;/code&gt; that exports &lt;code&gt;styles&lt;/code&gt;, &lt;code&gt;mount&lt;/code&gt; and &lt;code&gt;unmount&lt;/code&gt;. The layout adds a menu entry for every manifest it finds, and the runtime wires it up. Like the XP one, it only works where its query matches. Adding one doesn’t mean editing anything outside its folder.&lt;/p&gt;
&lt;p&gt;XP itself doesn’t meet that bar yet, which is a little embarrassing for the theme that’s supposed to be the example. The home page has a few empty slots for it to fill, and the app embed pages import its stylesheet straight out of the XP folder.&lt;/p&gt;
&lt;p&gt;In the tests, a stub theme that’s just its own folder goes through the same discovery and mounts with nothing else edited. Another test switches from XP to a stub and back to normal, and the page’s HTML has to come out exactly the way it went in.&lt;/p&gt;
&lt;p&gt;There’s still just the one theme. If a second one ever shows up, it gets a folder.&lt;/p&gt;
</content:encoded><category>meta</category><category>code</category></item><item><title>Time for a bigger site</title><link>https://krizdingus.com/blog/time-for-a-bigger-site/</link><guid isPermaLink="true">https://krizdingus.com/blog/time-for-a-bigger-site/</guid><description>The old krizdingus.com was one page, one giant scripts.js, and a manifesto that said &quot;No Astro.&quot; This one is built with Astro. We should probably talk about that.</description><pubDate>Tue, 20 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you visited krizdingus.com any time in the last year and a half, you got one page. A fake computer booted up at the top and typed out a bunch of nonsense like “Ego containment… STABLE,” then settled into a little window called KRIZDINGUS.EXE with menus that actually opened. Under that was a featured project that glitched every time it changed, a grid of more project cards, and an about section. Every card linked off to somewhere else.&lt;/p&gt;
&lt;p&gt;That site came with a manifesto. It was called &lt;code&gt;PHILOSOPHY.md&lt;/code&gt;, it sat right there in the site’s code, and it explained at length why everything was built the way it was. One section was called “The Stack: Light but Real,” and in it I wrote, with my whole chest:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;No React. No Vue. No Astro. Not even jQuery.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This site is built with Astro.&lt;/p&gt;
&lt;p&gt;So yeah. Let’s talk about that.&lt;/p&gt;
&lt;h2 id=&quot;scoring-the-old-list&quot;&gt;Scoring the old list&lt;/h2&gt;
&lt;p&gt;React, still no. Vue, still no. jQuery, still no. Astro… well, you know. Three out of four isn’t terrible, unless you keep reading the old manifesto, which also bragged about “No imports. No TypeScript. No bundler.” Those all went too. If you’re keeping score at home, it’s not looking great for past me.&lt;/p&gt;
&lt;p&gt;In my defense, the manifesto was about a one-page site, and a one-page site really doesn’t need anything. It’s a list. The old stack was, in its own words, “a cold sandwich eaten over a workbench, standing up,” and for a list, a cold sandwich is plenty.&lt;/p&gt;
&lt;p&gt;The problem was the list. The old manifesto even admitted it, with a whole section called “Projects: A Monument to External Links.” Eight cards, eight links to MakerWorld or GitHub or Thingiverse, and a featured-project rotator that was 200-some lines of JavaScript, all so you could be told “Look, I 3D printed a poop stool” and sent somewhere else to actually read about it.&lt;/p&gt;
&lt;h2 id=&quot;why-it-needed-a-bigger-one&quot;&gt;Why it needed a bigger one&lt;/h2&gt;
&lt;p&gt;The one-page idea was fine. It was just time for a bigger site.&lt;/p&gt;
&lt;p&gt;Now every project gets its own page, with the writeup, the photos, the print settings, and the parts that went wrong. There’s a blog, which you’re reading. And there’s a page about the web work I do for a living, which used to be a whole separate site. I don’t keep that one anymore. It’s all just this site now.&lt;/p&gt;
&lt;p&gt;That’s a lot of pages, and the old way of doing things was one HTML file and a &lt;code&gt;scripts.js&lt;/code&gt; that ran to 1,863 lines. Doing all of this by hand would’ve gotten ugly fast.&lt;/p&gt;
&lt;p&gt;Which is where Astro comes in. In plain terms, it takes a folder of text files and some templates and builds the pages out of them ahead of time, on my computer. What you download is finished HTML and CSS, the same kind of plain files the old site was. Astro just does the tedious part before you ever show up.&lt;/p&gt;
&lt;p&gt;The funny thing is, my favorite line from the old manifesto survived the switch completely: “If I want to update a project, I open a text file and type words like it’s 1998.” Still true. Every project and every post on here is a text file. There’s just a robot now that turns them into pages.&lt;/p&gt;
&lt;h2 id=&quot;what-happened-to-the-glitching&quot;&gt;What happened to the glitching&lt;/h2&gt;
&lt;p&gt;The old site was loud by default. You showed up and it booted at you, whether you wanted that or not.&lt;/p&gt;
&lt;p&gt;Now the default is normal mode, which is just the site: pages, text and pictures. It works fine with JavaScript turned off, and mostly it sits still. The one thing that moves is a field of little pixels drifting in the margins, and only if your screen is wide enough to have margins.&lt;/p&gt;
&lt;p&gt;The fake computer didn’t go anywhere, though. It grew up into XP mode, a whole Windows XP-style desktop you can switch on, with windows, a taskbar, a Start menu and a boot screen. Why XP? That’s when I was on the computer the most, and it’s when the internet really took off for everybody. For nerds and web people, Windows XP is basically the shag carpet of the ’70s.&lt;/p&gt;
&lt;p&gt;The old site’s leftovers moved in there too: the boot sequences (one of them is still called Terminal Crimewave), the wallpapers, the scanlines, and the animations with names like &lt;code&gt;chromaBleed&lt;/code&gt; and &lt;code&gt;crtPowerOff&lt;/code&gt;. Normal mode doesn’t even download any of that. It only loads when you pick XP mode from the menu next to the light and dark settings, and that option only shows up on a wide screen with a mouse or trackpad. Phones never get it.&lt;/p&gt;
&lt;p&gt;Why flip it around? Plenty of people have done the fake-desktop gimmick by now, so it’s not exactly new, and not everybody’s into it. Some people come here for the more serious stuff, and they shouldn’t have to dig through a Start menu to find it. So the default is the version that gets the information across, and the desktop is there for the people who want it. If you want to know how that switch is built, &lt;a href=&quot;https://krizdingus.com/blog/a-theme-is-a-folder/&quot;&gt;A theme is a folder&lt;/a&gt; gets into it.&lt;/p&gt;
&lt;h2 id=&quot;the-stuff-that-didnt-change&quot;&gt;The stuff that didn’t change&lt;/h2&gt;
&lt;p&gt;It’s still a static site. Static means the pages are finished files sitting on a host, and nothing runs on a server when you show up. Okay, full honesty: for a while in the middle of all this, there was a server, and a Dockerfile, and the whole deal. That’s extra funny, because the old manifesto described Cloudflare Pages as “blissfully unaware of what a Docker container is.” It didn’t last. It’s back on Cloudflare Pages, as plain files, where the old site lived the whole time.&lt;/p&gt;
&lt;p&gt;There’s still a light mode and a dark mode. Dark is a deep plum now, light is warm paper, and each section of the site gets its own color.&lt;/p&gt;
&lt;p&gt;I’m still overthinking the &lt;code&gt;_redirects&lt;/code&gt; file. The old manifesto made fun of me for sending &lt;code&gt;/mousebitelabs&lt;/code&gt; to a GitHub repo nobody had ever visited, “like a digital doorman at an abandoned building.” Good news for the doorman. He has a building now. That old link goes to its own project page.&lt;/p&gt;
&lt;p&gt;The ASCII art system didn’t make the move, though. That was a whole setup for rotating pixel art, with the timing and the fade effects all done, and not a single drawing, because, in the old manifesto’s words, I have “the artistic ability of a moderately intelligent doorknob.” It sat in the old code doing nothing, and it stayed there. I don’t think anybody will notice.&lt;/p&gt;
&lt;h2 id=&quot;the-scores&quot;&gt;The scores&lt;/h2&gt;
&lt;p&gt;The old manifesto had a lot to say about Lighthouse, which is Google’s tool for grading a web page on speed, accessibility and the rest. The old site scored in the 60s and 70s, and the old manifesto treated that like a feature. “My circles are yellow-orange and they flicker occasionally,” I wrote. “Just like nature intended.”&lt;/p&gt;
&lt;p&gt;This one scores 99 and 100 on a phone. Turns out the trick was not making everybody download every glitch animation I’ve ever written just to look at a list. My circles are green now. I don’t know what to tell you.&lt;/p&gt;
&lt;p&gt;The analytics changed too. The old site ran Google Analytics. This one uses Cloudflare’s, which counts visits without cookies and without following you around the internet.&lt;/p&gt;
&lt;h2 id=&quot;six-months-give-or-take&quot;&gt;Six months, give or take&lt;/h2&gt;
&lt;p&gt;The old manifesto ended with “And if I rebuild it again in six months? Good. That means I’m still making things.”&lt;/p&gt;
&lt;p&gt;It was more like a year and a half. Close enough.&lt;/p&gt;
&lt;p&gt;Most of that old write-up was about a one-page site, so most of it doesn’t apply anymore. What carried over is the energy behind it, which comes down to fuck it, we ball. That’s still how this one got made, just with more pages and, apparently, Astro.&lt;/p&gt;
</content:encoded><category>meta</category></item><item><title>The texture lives in the profile</title><link>https://krizdingus.com/blog/the-texture-lives-in-the-profile/</link><guid isPermaLink="true">https://krizdingus.com/blog/the-texture-lives-in-the-profile/</guid><description>The ribs on my threaded Zyn can showed up on the test print. They aren&apos;t in the model, and I still couldn&apos;t tell you how they got there.</description><pubDate>Wed, 09 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This one needs a little setup, since it’s about nicotine pouches and 3D printer settings, and I’m guessing not a lot of people are into both.&lt;/p&gt;
&lt;p&gt;Zyn is a brand of nicotine pouches, the little white ones you tuck under your lip. They come in a small round plastic can. The &lt;a href=&quot;https://krizdingus.com/projects/zyn-container-easy-open-threaded/&quot;&gt;threaded Zyn container&lt;/a&gt; is my printed version of that can. It’s the same diameter and depth as the real one, so it fits anywhere the real one fits, but the lid screws on, and the inside is split: two-thirds for fresh pouches, one-third for used ones or a second flavor. Two parts, a base and a lid. No glue, no magnets.&lt;/p&gt;
&lt;p&gt;It was the first Zyn container I published, and one of the first things I ever modeled. I got into Game Boy modding around 2022 and 3D printing came right after, so I hadn’t been drawing my own stuff for very long. I made it because I thought it’d be neat to have a styled one, and a place to keep your discards. I go through a lot, so it’s not always convenient to find a trash can.&lt;/p&gt;
&lt;p&gt;The outside of it has a ribbed grip texture. I didn’t put that there. That’s the whole story, kind of, but it makes more sense if you know how a print actually happens.&lt;/p&gt;
&lt;h2 id=&quot;shape-and-settings&quot;&gt;Shape and settings&lt;/h2&gt;
&lt;p&gt;You draw the thing in a 3D modeling app, which people call CAD. I use Shapr3D. Then you export the shape, usually as an STL file, which is just the outside surface of the object described as a mesh of tiny triangles. It doesn’t know anything about how to print it, it’s just the shape.&lt;/p&gt;
&lt;p&gt;That file goes into a slicer, which is a program that cuts the model into thin horizontal layers and works out where the nozzle goes on every one of them, and how much melted plastic comes out along the way. It’s where you pick everything: layer height, how many walls, how much infill. Infill is what’s inside a part. Most prints aren’t solid, they’re a couple of outer walls with a sparse pattern filling the middle so you’re not burning plastic on stuff nobody sees. This can is two walls and 15% infill, which is pretty normal. It’s a lot of knobs for a plastic can, but that’s how it goes.&lt;/p&gt;
&lt;p&gt;Save all those choices and you’ve got a print profile. MakerWorld is the model-sharing site run by Bambu Lab, the printer company, and it lets you upload your profile right next to the files, so somebody else can hit print and get the settings you used.&lt;/p&gt;
&lt;p&gt;And those settings can change what the outside looks like, since the slicer decides how every line of plastic goes down.&lt;/p&gt;
&lt;h2 id=&quot;the-test-print&quot;&gt;The test print&lt;/h2&gt;
&lt;p&gt;I modeled a smooth wall. The test print had ribs on it. I looked at it, said fuck it, that’s cool, and kept them.&lt;/p&gt;
&lt;p&gt;The difference turned out to be a setting the slicer calls infill gap, in its Other tab. It was at 100% instead of 150%. Which is a little backwards, since I just told you infill is the stuff on the inside, and here’s an infill setting putting ribs on the outside wall.&lt;/p&gt;
&lt;p&gt;How it got to 100%, I don’t know exactly. Either I messed with the settings by accident, or I dropped the STL into some other model’s print profile because that model’s file was already open and I didn’t realize. No idea which.&lt;/p&gt;
&lt;p&gt;I never thought about going back and modeling the ribs into the file, either. The STL, STEP and OBJ on MakerWorld are all the smooth can I drew. OBJ is just another common 3D format, and STEP is the one you’d want if you’re going to edit the actual shape. Print from my profile and you get ribs. Slice the raw STL with your own settings and the wall comes out plain. If you want to remix it, which is MakerWorld’s word for making your own version of somebody’s model, and you want the ribs back, infill gap at 100% is the setting I know is different, so that’s where I’d start. Or just print from the profile.&lt;/p&gt;
&lt;p&gt;It’s a little funny to me that the outside I liked better than the one I drew isn’t in the model at all.&lt;/p&gt;
&lt;h2 id=&quot;printing-one&quot;&gt;Printing one&lt;/h2&gt;
&lt;p&gt;It’s printed in PETG, one of the standard printing plastics, at 0.16 mm layers, and takes about three and a half hours. No supports, either, so the slicer doesn’t have to build any throwaway scaffolding under overhangs that you’d snap off after. The threads are printed too, so they’re tight for the first few opens. Give it some turns and a rinse and they loosen up.&lt;/p&gt;
&lt;p&gt;People have done it in ABS, and in clear PETG with a colored lid, and the two-tone ones look good.&lt;/p&gt;
&lt;p&gt;What’s stuck with me from the comments is how well received it is. As of September 8 it had a 4.9 from 51 ratings, with 95 comments, and 490 people had saved it. I’ll take it.&lt;/p&gt;
&lt;p&gt;The one that stuck with me was a guy who printed a set for the guys at his fire station, and said they were stoked. My listing even says a fire department fundraiser is exactly the kind of thing I’m happy to see. The license is non-commercial, and I don’t want people selling them on Etsy as their own, but that’s a whole different thing. It made me really thrilled that I somehow got to help some community firehouse just by making some dumb shit. In some small way I got to contribute without even trying.&lt;/p&gt;
</content:encoded><category>3d-printing</category></item><item><title>Robots saw right through that shit</title><link>https://krizdingus.com/blog/robots-saw-right-through-that/</link><guid isPermaLink="true">https://krizdingus.com/blog/robots-saw-right-through-that/</guid><description>NotebookLM will turn your documents into a podcast with two AI hosts. A fake memo out of Google Docs didn&apos;t fool them, so I wrote a Python tool that makes fake memos look photocopied, and the next one did.</description><pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Okay, so if you’ve never messed with NotebookLM: it’s a Google tool where you upload your documents, PDFs, web pages, whatever, and it reads them for you. You can ask it questions about your stuff. It also has a button that turns all of it into a podcast, two AI hosts talking through whatever you gave them like it’s their show.&lt;/p&gt;
&lt;p&gt;A friend sent me one of those in June 2025. Two hosts, 15 minutes, on an article from Wookieepedia, the Star Wars wiki. I was tickled. The funniest shit to me was how earnestly they went through it, citing sources and cross-referencing, without any trace of irony, like it was a legit source. And I wondered if I could trick these things.&lt;/p&gt;
&lt;h2 id=&quot;round-one&quot;&gt;Round one&lt;/h2&gt;
&lt;p&gt;That same afternoon I made an FBI memo in Google Docs, exported it as a PDF and uploaded it.&lt;/p&gt;
&lt;p&gt;Robots saw right through that shit.&lt;/p&gt;
&lt;p&gt;I guess a memo that came out of Google Docs looks like a memo that came out of Google Docs. The hosts were clearly tongue-in-cheek about it, with an &lt;em&gt;okay, sure, buddy&lt;/em&gt; attitude.&lt;/p&gt;
&lt;p&gt;I took that as a challenge. They treated Star Wars as legit, so why not this? What if it looked like a real source?&lt;/p&gt;
&lt;p&gt;That’s where leaksmith came from. It’s a Python command-line tool, so no windows or buttons, you run it by typing in a terminal. You write your memo in markdown, which is plain text with a few symbols for headings and bold and lists, and put the title, agency, classification and date at the top. leaksmith lays it out like an FBI memo and makes a PDF. Then, if you ask it to, it beats that PDF up until it looks like it’s been copied and scanned a bunch of times.&lt;/p&gt;
&lt;p&gt;Officially, it exists for testing whether AI systems can tell fake documents from real ones. Same question, just with a tie on. &lt;a href=&quot;https://krizdingus.com/projects/leaksmith/&quot;&gt;The project page&lt;/a&gt; has how it all works. This is more about how it went.&lt;/p&gt;
&lt;h2 id=&quot;seventeen-days-of-this&quot;&gt;Seventeen days of this&lt;/h2&gt;
&lt;p&gt;Two days later I started the actual Python project, and the tests were failing almost right away. The worst one was the date. The parser, the part that reads those labeled lines at the top of the memo, was reading the date as an actual date, the kind of value code does math with, and the tests expected plain text. That one ate hours. Once the dates were text again, the markdown library broke a few more tests, and it was the next afternoon before everything passed.&lt;/p&gt;
&lt;p&gt;WeasyPrint was next, the library that turns the memo into a PDF. It works by rendering a web page, so before any of the aging, the memo is a web page. And it wouldn’t even import, meaning Python couldn’t load it at all, because a library it depends on was nowhere to be found. I installed a whole stack of graphics libraries with Homebrew, the package manager a lot of Mac developers use, and it still failed. The fix turned out to be two environment variables, basically two lines telling Python where Homebrew had put everything. I should have tried that hours earlier.&lt;/p&gt;
&lt;p&gt;Then I did the FBI template, Courier New with specific margins and red classification boxes, and now markdown went in one end and a PDF came out the other. The command line was the last fight. Typer, the framework I used for it, wanted named options like &lt;code&gt;--input memo.md&lt;/code&gt; instead of just the filename, or at least that’s how I got it working. That’s why you type &lt;code&gt;leaksmith render run --input memo.md&lt;/code&gt; instead of something shorter. Longer than I wanted, but fine. After one more fight with the install, I could actually run it.&lt;/p&gt;
&lt;h2 id=&quot;making-it-look-old&quot;&gt;Making it look old&lt;/h2&gt;
&lt;p&gt;So the PDFs worked, and they looked fake. Way too clean. They had to get roughed up, and the plan was four steps: turn every page into an image, add noise and blur, draw some random lines across it, and turn it back into a PDF.&lt;/p&gt;
&lt;p&gt;ImageMagick, the command-line tool that does the turning-back-into-a-PDF part, comes in two versions, 6 and 7, and they disagree about syntax. Of course it couldn’t just be simple. I had to make it work with both, and then it worked. After that it was tuning: go light and the page still looks brand new, go heavy and nobody can read it.&lt;/p&gt;
&lt;h2 id=&quot;round-two&quot;&gt;Round two&lt;/h2&gt;
&lt;p&gt;For a test document I wrote a fake CIA memo about interdimensional communication research and ran it through. It came out looking old enough. Then I uploaded it to NotebookLM.&lt;/p&gt;
&lt;p&gt;The hosts spent 14 minutes on interdimensional protocols and treated it like a legitimate source. Holy shit, it actually worked. It was hilarious, though I can’t remember any specific lines now.&lt;/p&gt;
&lt;p&gt;The weird part is that it shouldn’t fool a person. The aging is uniform because a computer did it, the type is too clean, and the classification markings are generic. Anybody who’s seen real leaked documents would spot it immediately. No idea why the robots fall for it. They did when I built it, anyway.&lt;/p&gt;
&lt;h2 id=&quot;loose-ends&quot;&gt;Loose ends&lt;/h2&gt;
&lt;p&gt;The last couple of days were paperwork: a README, the page that explains the project, a license saying what people can do with the code, and a legal file full of disclaimers. Then the ImageMagick 6-versus-7 thing bit me again on a different computer, so I fixed that again, and in early July it went up on GitHub.&lt;/p&gt;
&lt;p&gt;All in, it was 17 days from a Star Wars podcast to a tool that turns markdown into aged PDFs that fool AI.&lt;/p&gt;
</content:encoded><category>code</category><category>ai</category></item><item><title>Turns out it’s not 100,000 games</title><link>https://krizdingus.com/blog/turns-out-its-not-100000-games/</link><guid isPermaLink="true">https://krizdingus.com/blog/turns-out-its-not-100000-games/</guid><description>A Discord bot for looking up every Game Boy game, built over three days in May 2025. README jokes, a fix I undid and put back, and a number I can’t explain.</description><pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is a story about a Discord bot that searches a list of Game Boy games, and I realize every part of that sentence might need explaining. Discord’s the easy one. It’s a chat app where communities run their own servers, and a bot is a program that sits in one of those servers and answers commands. The rest takes a minute.&lt;/p&gt;
&lt;p&gt;A ROM is the game itself, the data off the chip inside the cartridge, copied into a file. No-Intro is a long-running project that catalogs those dumps and names them consistently, so the filename tells you the region and the revision. A guy who goes by Catskull took the whole No-Intro Game Boy set and turned it into one page you can search, the &lt;a href=&quot;https://catskull.net/gb-rom-database/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GB ROM Database&lt;span class=&quot;sr-only&quot;&gt; (opens in a new tab)&lt;/span&gt;&lt;/a&gt;. His GitHub repo describes it as “a complete list of all Gameboy games ever made.”&lt;/p&gt;
&lt;p&gt;Why would anybody need to search that? Because if you’re making a reproduction cartridge, or writing a game onto a rewritable one (people call that flashing it), the title is the least of it. You want the mapper, which is the chip in the cartridge that lets the Game Boy reach more memory than it was built to see. You want to know whether it saves to a battery, whether it has a clock or rumble, whether it’s a Game Boy Color game or has Super Game Boy extras. That’s my crowd.&lt;/p&gt;
&lt;h2 id=&quot;the-plan-was-a-website&quot;&gt;The plan was a website&lt;/h2&gt;
&lt;p&gt;I meant to build a nicer front end for Catskull’s database. His page could’ve been better set up for the kinds of things I wanted to search for, and frankly, I just wanted to see if I could do better. I did build one, the Flash Cartridge Assistant, a search page that sat on this site in 2024. That’s the project this whole story grew out of. But plenty of people had built a search page, Catskull included. So I figured I’d better try something different and put it in Discord, since that’s where the people asking those questions already are.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://krizdingus.com/projects/gb-rom-database-bot/&quot;&gt;GB ROM Database Bot&lt;/a&gt; answers &lt;code&gt;/dmgdb&lt;/code&gt; (DMG is the original Game Boy’s model number). Type &lt;code&gt;/dmgdb search zelda&lt;/code&gt; and you get the title, region, mapper and the rest, with filters for all that cart stuff.&lt;/p&gt;
&lt;p&gt;I built it over three days in May 2025, and I didn’t do it neatly.&lt;/p&gt;
&lt;h2 id=&quot;everything-at-once-some-of-it-twice&quot;&gt;Everything at once, some of it twice&lt;/h2&gt;
&lt;p&gt;I dumped the whole bot into its GitHub repo in one go: the search code, the Python script that builds the game list, and the game list itself, which runs about 89,000 lines. Twice. There was a file called &lt;code&gt;rom-list copy.json&lt;/code&gt; sitting in the folder, and it went up right next to the real one. I took it back out a few minutes later.&lt;/p&gt;
&lt;p&gt;The README, the page that introduces a project on GitHub, got a roadmap of things I was going to add, and most of them never happened. One item had GitHub Actions (GitHub’s way of running scripts for you on a schedule) refreshing your ROM list “while you’re busy pretending you’re still a millennial.” That night I changed it to “pretending you watch DBZ AMVs,” then changed it again about thirty seconds later to just “busy watching DBZ AMVs,” meaning Dragon Ball Z clips cut to music. Coming from a guy who used to make GeoCities pages about Gohan, neither version was much of a burn. There’s a second millennial joke down in the disclaimer that I never touched.&lt;/p&gt;
&lt;h2 id=&quot;nearly-500-games&quot;&gt;Nearly 500 games&lt;/h2&gt;
&lt;p&gt;The next day the bot got a card on my old site, which was one page with a card for each project: “Because we absolutely needed another database of nearly 500 Game Boy games to search through (but this one has filtering!).”&lt;/p&gt;
&lt;p&gt;I think the 500 was a typo. I bumped it to 5000 that evening and labeled the fix “turns out it’s not 100,000 games, who knew?” Where the 100,000 came from, I can’t tell you. Nothing on that card, in the bot, or anywhere else on the old site ever said it. The bot’s list has 4,469 games. Catskull’s README says 4,351 unique ones once he cleared out the duplicates. So it’s around four and a half thousand whichever list you trust, and nowhere near 100,000. Nearly 5000 is generous, but fine.&lt;/p&gt;
&lt;p&gt;While we’re on numbers I can’t explain: the bot has a changelog, the running list of what changed in each version, and every version in it, 1.0.0 through 1.0.5, is dated March 19, 2024. That’s more than a year before I wrote any of it. I probably typed 2024 when I meant 2025, but March 19 would still be two months before I started, so I’ve got nothing on that one.&lt;/p&gt;
&lt;h2 id=&quot;the-share-button&quot;&gt;The share button&lt;/h2&gt;
&lt;p&gt;The thing I kept coming back to after day one was one button. When you search, the bot answers so only you can see it (Discord calls that an ephemeral message), and there’s a Share to Channel button if you want everybody else to see what you found. For that to work, the bot has to remember what you searched, and it only remembered for a minute.&lt;/p&gt;
&lt;p&gt;I’d put that button on the random game command that first evening. The next morning I fixed a “search expired” error that turned up sometimes when you shared right after searching, and part of the fix was letting a search live for 70 seconds instead of 60. Ten whole extra seconds. Then I gave the random game command the same memory, since its share button had the same problem.&lt;/p&gt;
&lt;p&gt;That night I was after something else. Discord has to be told what commands a bot has, and you can register them to one server, which is instant, or everywhere, which can take up to an hour. I was having trouble getting that part to behave, so I added a pile of logging to figure out what was going on. Then I undid the logging and the random game fix from that morning, both at once.&lt;/p&gt;
&lt;p&gt;The next morning I got the commands showing up in new servers again, and that was 1.0.4. Then I put the random game fix back, and that was 1.0.5. So that’s the same fix three times: in, out, and back in, a little further down.&lt;/p&gt;
&lt;p&gt;That was the end of it. It worked, and I probably got busy with something else.&lt;/p&gt;
&lt;p&gt;It’s still running. It lives on the &lt;a href=&quot;https://mousebitelabs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mouse Bite Labs&lt;span class=&quot;sr-only&quot;&gt; (opens in a new tab)&lt;/span&gt;&lt;/a&gt; Discord, a very active community run by BucketMouse, who’s a well-regarded maker and modder. If you’re into modding Game Boys, I highly recommend that server. The bot’s also on my own test server. I host it myself, and there’s no public copy to invite. The code’s &lt;a href=&quot;https://github.com/krizdingus/gb-rom-database-bot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;on GitHub&lt;span class=&quot;sr-only&quot;&gt; (opens in a new tab)&lt;/span&gt;&lt;/a&gt; if you want to run your own.&lt;/p&gt;
</content:encoded><category>game-boy</category><category>discord</category></item></channel></rss>