More from macwright.com
I watched a large part of All Watched Over By Machines of Loving Grace this month. This also counts as a “listening” item, because the theme song, “Baby Love Child” by Pizzicato Five, is also spectacular. Guitar Moves is a good series of interviews by Matt Sweeney, who I mostly know via his involvement in Bonnie Prince Billy. It’s a really cool format. I like how he interviews guitarists with recognizable sounds, and you get to see how little they need to play to sound just like themselves. The episode with St. Vincent is excellent too: she’s one of my guitar heroes: check out the guitar solo in Just The Same But Brand New, or her version of Dig a Pony. I also watched No Other Land. Everyone should watch No Other Land. AI thoughts roundup I don’t have a conclusion. Really, that’s my current state: ambivalence. I acknowledge that these tools are incredibly powerful, I’ve even started incorporating them into my work in certain limited ways (low-stakes code like POCs and unit tests seem like an ideal use case), but I absolutely hate them. I hate the way they’ve taken over the software industry, I hate how they make me feel while I’m using them, and I hate the human-intelligence-insulting postulation that a glorified Excel spreadsheet can do what I can but better. Nolan Lawson: AI ambivalence As I always say, the purpose of the system is what it does. Or, in this case, how I think about AI stuff is mostly affected by how people use AI stuff, and how people use AI stuff is a real mixed bag. There’s the tidal wave of spam, the aesthetic of fascism, the low-effort marketing materials with nonsense images, the non-consensual AI porn. I see all of the bad stuff every day both online and in the odd subway ad. The good stuff seems pretty theoretical, though: the press releases about AI-driven medical advances never seem to break into the real world. The stories about engineers 10x’ing their ability seem pretty mixed: we’re already at the hangover-and-regret phase with programmers bemoaning how they’ve generated so much slop and lost so much knowledge. Anyway, I’m mildly optimistic about the potential! But it’s a lot like crypto in that you could theoretically use the technology for something good but most people loudly used it for bad stuff, and people including me judged it based on what it did. AI has to start doing some good stuff soon. Potential isn’t enough. I think one thing chatGPT’s invention has revealed is how many people - including some very important people in society - find just basic reading and writing to be laborious and cumbersome to perform, and how oddly closely that type of strained literacy correlates with having other shitty opinions. From mtsw on Bluesky, about this story about Andrew Cuomo using ChatGPT to half-assedly write a policy platform. Right off the bat I should say that judging people for their level of literacy reeks of classism and so on. My own ability to read & write has a lot to do with my place in society: I went to good schools, had a stable home life, and smart parents. However, “the way that society was set up” kind of evened this out. Extremely social people with cultural capital and chiseled jawlines and biceps would get their rewards, and people like… myself, we would get rewarded for literacy and critical thinking. When one group needed the other, it was usually some kind of payment or partnership: Cuomo pays his scriptwriter, the TV show creator pays the actors. And some people can do both sides of the equation. But LLMs definitely indicate that people do not like this deal. Whew, they don’t like writing, but they also don’t like paying the writers or reading what they write. Maybe they could rejigger the system so that they could do it all. They have ideas for music and art but no interest in learning about music, practicing instruments, going to art school, or concentrating on a task for a long time, so why not generate it all? Why not, well - there are reasons, those reasons being that the generated output usually passes their own vibe check but once someone who looks closely at things or reads all the words encounters it, everyone points at the slop and it’s embarrassing. (Cuomo will never be embarrassed) Plus, you’re always going to get average results by asking a device that is incapable of creativity or thought. Also, you’ll miss out on the human experience of creating. And you’ll be indirectly feeding output data into training data for future LLMs, consequentially making their output worse. (Cuomo does not care about consequences) Colophon update I’ve moved the images for this website to Bunny (that’s an affiliate link, here’s a non-affiliate link if that’s what you prefer). When I initially moved my photos to this website, I set them up with Amazon S3 for storage and CloudFront to serve them with a CDN. Using AWS is painful for me, so I moved them to Cloudflare R2, which is Cloudflare’s equivalent to S3, and Cloudflare as a CDN. Thanks to owning my own domains, swapping out image hosts is pretty quick: switching to Bunny took all of five minutes. So what’s the deal with Bunny? Partly I’ve become a little more negative on Cloudflare and R2: I think Cloudflare’s technology is neat, but R2 has iffy reliability and Cloudflare has iffy politics. I’m also intrigued by diversifying my dependencies geographically. Bunny is a Slovenian company, and my email is from an Australian company. This probably won’t have any practical effect, but it feels kind of good for obvious reasons to even minutely hedge my bets here. So far Bunny has been great. They don’t support the S3 protocol but they do support SFTP, which works just as well for my purposes and works great with the beautiful Transmit app. Before, with R2, I was using the significantly less beautiful Cyberduck application because Cloudflare R2 doesn’t support all of the S3 protocol. It seems to be just as fast as Cloudflare was, too. And I’m somewhat reassured by the prospect of paying Bunny. I don’t like the feeling of getting “free” services like I can from Cloudflare. I want that customer relationship. Reading Then again, pop culture is powerful, and even the dumbest marketing both affects and reflects it. Busch Light’s can holder shaped like a cup that holds beer is dumb, which is fine, because most beer promos are. But the fact that the brand frames it as a functional, masculine alternative to Stanley’s H2.0 Flowstate affirms a similarly retrograde outlook on gender roles to the one that young American men are seeking out on the political right. From my friend Dave’s article about Busch Light’s weird attempt to riff on the Stanley Tumbler trend. I was once a loyal listener of the Chapo Trap House podcast, but fell off of it in 2020 when their support of Bernie Sanders led them to be jerks about Elizabeth Warren. But reading this Vanity Fair article about the cohosts of the podcast endeared them to me a bit. “Like to thank” is linguistic phlegm. “I’d like to thank the Academy.” They’d “like to thank” me. Well I’d like to be 6’3” and drive a G Wagon, thanks. I’d like you to accept my novella. I’d like to quit paying three dollars to Submittable every time I want to send a story out. The world is full of actions I would like to do. The most direct way to say thank you is just to say it: “thank you, name, for doing X.” “I’d like to thank” is a performative thanks, a thanks with a smirk and a blink, eyeing for extra credit. Just because people say it in their award show acceptance speeches doesn’t mean you should say it, too. In fact, that’s the reason you shouldn’t say it. Loved this article “Close reading my rejections” from friend of the blog Barrett Hathcock.
I used to make little applications just for myself. Sixteen years ago (oof) I wrote a habit tracking application, and a keylogger that let me keep track of when I was using a computer, and generate some pretty charts. I’ve taken a long break from those kinds of things. I love my hobbies, but they’ve drifted toward the non-technical, and the idea of keeping a server online for a fun project is unappealing (which is something that I hope Val Town, where I work, fixes). Some folks maintain whole ‘homelab’ setups and run Kubernetes in their basement. Not me, at least for now. But I have been tiptoeing back into some little custom tools that only I use, with a focus on just my own computing experience. Here’s a quick tour. Hammerspoon Hammerspoon is an extremely powerful scripting tool for macOS that lets you write custom keyboard shortcuts, UIs, and more with the very friendly little language Lua. Right now my Hammerspoon configuration is very simple, but I think I’ll use it for a lot more as time progresses. Here it is: hs.hotkey.bind({"cmd", "shift"}, "return", function() local frontmost = hs.application.frontmostApplication() if frontmost:name() == "Ghostty" then frontmost:hide() else hs.application.launchOrFocus("Ghostty") end end) Not much! But I recently switched to Ghostty as my terminal, and I heavily relied on iTerm2’s global show/hide shortcut. Ghostty doesn’t have an equivalent, and Mikael Henriksson suggested a script like this in GitHub discussions, so I ran with it. Hammerspoon can do practically anything, so it’ll probably be useful for other stuff too. SwiftBar I review a lot of PRs these days. I wanted an easy way to see how many were in my review queue and go to them quickly. So, this script runs with SwiftBar, which is a flexible way to put any script’s output into your menu bar. It uses the GitHub CLI to list the issues, and jq to massage that output into a friendly list of issues, which I can click on to go directly to the issue on GitHub. #!/bin/bash # <xbar.title>GitHub PR Reviews</xbar.title> # <xbar.version>v0.0</xbar.version> # <xbar.author>Tom MacWright</xbar.author> # <xbar.author.github>tmcw</xbar.author.github> # <xbar.desc>Displays PRs that you need to review</xbar.desc> # <xbar.image></xbar.image> # <xbar.dependencies>Bash GNU AWK</xbar.dependencies> # <xbar.abouturl></xbar.abouturl> DATA=$(gh search prs --state=open -R val-town/val.town --review-requested=@me --json url,title,number,author) echo "$(echo "$DATA" | jq 'length') PR" echo '---' echo "$DATA" | jq -c '.[]' | while IFS= read -r pr; do TITLE=$(echo "$pr" | jq -r '.title') AUTHOR=$(echo "$pr" | jq -r '.author.login') URL=$(echo "$pr" | jq -r '.url') echo "$TITLE ($AUTHOR) | href=$URL" done Tampermonkey Tampermonkey is essentially a twist on Greasemonkey: both let you run your own JavaScript on anybody’s webpage. Sidenote: Greasemonkey was created by Aaron Boodman, who went on to write Replicache, which I used in Placemark, and is now working on Zero, the successor to Replicache. Anyway, I have a few fancy credit cards which have ‘offers’ which only work if you ‘activate’ them. This is an annoying dark pattern! And there’s a solution to it - CardPointers - but I neither spend enough nor care enough about points hacking to justify the cost. Plus, I’d like to know what code is running on my bank website. So, Tampermonkey to the rescue! I wrote userscripts for Chase, American Express, and Citi. You can check them out on this Gist but I strongly recommend to read through all the code because of the afore-mentioned risks around running untrusted code on your bank account’s website! Obsidian Freeform This is a plugin for Obsidian, the notetaking tool that I use every day. Freeform is pretty cool, if I can say so myself (I wrote it), but could be much better. The development experience is lackluster because you can’t preview output at the same time as writing code: you have to toggle between the two states. I’ll fix that eventually, or perhaps Obsidian will add new API that makes it all work. I use Freeform for a lot of private health & financial data, almost always with an Observable Plot visualization as an eventual output. For example, when I was switching banks and one of the considerations was mortgage discounts in case I ever buy a house (ha 😢), it was fun to chart out the % discounts versus the required AUM. It’s been really nice to have this kind of visualization as ‘just another document’ in my notetaking app. Doesn’t need another server, and Obsidian is pretty secure and private.
I have a non-recently post ready to write, any day now… Reading This was a strong month for reading: I finished The Hidden Wealth of Nations, Useful Not True, and Cyberlibertarianism. I had a book club that read Cyberlibertarianism so we discussed it last week. I have a lot of qualms with the book, and gave it two stars for that reason. But I will admit that it’s taking up space in my mind. The ‘cyberlibertarian’ ideology was familiar to me before reading it. The book’s critique of it didn’t shift my thinking that much. But I have been thinking a lot about what it argued for, which is a world in which the government has very extensive powers – to limit what is said online, to regulate which companies can even create forums or social media platforms. He also believed that a government should be able to decrypt and read conversations between private citizens. It’s a very different idea of government power than what I’m used to, and well outside my comfort zone. I think it’s interesting to consider these things: the government probably should have some control of some kinds of speech, and in some cases it’s useful to have the FBI tapping the phones of drug smugglers or terrorists. How do we really define what’s acceptable and what isn’t? I don’t know, I want to do more thinking about the uncomfortable things that nevertheless may be necessary for functioning of society. Besides that, there is so much to read. This month I added a lot of news subscriptions to my pile, which I think is now Hell Gate, Wired, NYTimes, Bloomberg, 404 Media, The Verge, and a bunch of newsletters. This interview with Stephanie Kelton, who is at the forefront of the Modern Monetary Theory movement in America, and wrote the very good book The Deficit Myth. This 404 Media story on an AI-generated ‘true crime’ YouTube channel is great because the team at 404 Media does both deep research and they interrogate their sources. Nathan Tankus has always been good but in this era he’s essential reading. His piece on Fort Knox is quick and snappy. His others are more involved but always worth reading. Listening We’ve been rewatching The Bear and admiring the dad-rock soundtrack. This Nine Inch Nails track shows up at the end of a season: And this Eno track: Besides that, this track from Smino played at a local cocktail bar. The bars at 0:45 sound like they’re tumbling downhill in a delightful way. Watching So I bought a sewing machine in February, a beautiful old Kenmore 158-series, produced in the 1970s in Japan. It’s awesome. How sewing machines work is amazing, as this video lays out. There’s so much coordinated motion happening for every stitch, and the machines are so well-designed that they last for decades easily. Besides that, I just watched The Apprentice, which I really did not like. Elsewhere I was on a podcast with Jeremy Jung, taking about Placemark! My post in the /micro/ section, All Hat No Cowboy, probably could have or should have been a blog post, but I was feeling skittish about being too anti-AI on the main.
I am not going to repeat the news. But man, things are really, really bad and getting worse in America. It’s all so unendingly stupid and evil. The tech industry is being horrible, too. Wishing strength to the people who are much more exposed to the chaos than I am. Reading A Confederacy of Dunces was such a perfect novel. It was pure escapism, over-the-top comedy, and such an unusual artifact, that was sadly only appreciated posthumously. Very earnestly I believe that despite greater access to power and resources, the box labeled “socially acceptable ways to be a man” is much smaller than the box labeled “socially acceptable ways to be a woman.” This article on the distinction between patriarchy and men was an interesting read. With the whole… politics out there, it’s easy to go off the rails with any discussion about men and women and whether either have it easy or hard. The same author wrote this good article about declining male enrollment in college. I think both are worth a read. Whenever I read this kind of article, I’m reminded of how limited and mostly fortunate my own experience is. There’s a big difference, I think, in how vigorously you have to perform your gender in some red state where everyone owns a pickup truck, versus a major city where the roles are a little more fluid. Plus, I’ve been extremely fortunate to have a lot of friends and genuine open conversations about feelings with other men. I wish that was the norm! On Having a Maximum Wealth was right up my alley. I’m reading another one of the new-French-economist books right now, and am still fascinated by the prospect of wealth taxes. My friend David has started a local newsletter for Richmond, Virginia, and written a good piece about public surveillance. Construction Physics is consistently great, and their investigation of why skyscrapers are all glass boxes is no exception. Watching David Lynch was so great. We watched his film Lost Highway a few days after he passed, and it was even better than I had remembered it. Norm Macdonald’s extremely long jokes on late-night talk shows have been getting me through the days. Listening This song by the The Hard Quartet – a supergroup of Emmett Kelly, Stephen Malkmus (Pavement), Matt Sweeney and Jim White. It’s such a loving, tender bit of nonsense, very golden-age Pavement. They also have this nice chill song: I came across this SML album via Hearing Things, which has been highlighting a lot of good music. Small Medium Large by SML It’s a pretty good time for these independent high-quality art websites. Colossal has done the same for the art world and highlights good new art: I really want to make it out to see the Nick Cave (not the musician) art show while it’s in New York.
I was just enjoying Simon Willison’s predictions and, heck, why not. 1: The web becomes adversarial to AI The history of search engines is sort of an arms race between websites and search engines. Back in the early 2000s, juicing your ranking on search engines was pretty easy - you could put a bunch of junk in your meta description tags or put some text with lots of keywords on each page and make that text really tiny and transparent so users didn’t notice it but Google did. I doubt that Perplexity’s userbase is that big but Perplexity users are probably a lot wealthier on average than Google’s, and there’s some edge to be achieved by getting Perplexity to rank your content highly or recommend your website. I’ve already noticed some search results including links to content farms. There are handful of startups that do this already, but the prediction is: the average marketing exec at a consumer brand will put some of their budget to work on fooling AI. That means serving different content to AI scrapers, maybe using some twist on Glaze and other forms of adversarial image processing to make their marketing images more tantalizing to the bots. Websites will be increasingly aware that they’re being consumed by AI, and they will have a vested interest in messing with the way AI ‘perceives’ them. As Simon notes in his predictions, AIs are gullible: and that’s before there are widespread efforts to fool them. There’s probably some way to detect an AI scraper, give it a special payload, and trick it into recommending your brand of razors whenever anyone asks, and once someone figures it out this will be the marketing trend of the decade. 2: Copyright nihilism breeds a return to physical-only media The latest lawsuit about Meta’s use of pirated books, allegedly with Mark Zuckerberg’s explicit permission, if true, will be another reason to lose faith in the American legal system’s intellectual property system entirely. We’ve only seen it used to punish individuals and protect corporations, regardless of the facts and damages, and there’s no reason to believe it will do anything different (POSIWID). The result, besides an uptick in nihilism, could be a rejuvenation of physical-only releases. New albums only released on vinyl. Books only available in paperback format. More private screenings of hip movies. When all digital records are part of the ‘training dataset,’ a niche, hipster subset will be drawn to things that aren’t as easily captured and reproduced. This is parallel, to the state of closed-source models from Anthropic or OpenAI. They’re never distributed or run locally. They exist as bytes on some hard drive and in some massive GPU’s memory in some datacenter, and there aren’t Bittorrents pirating them because they’re kept away from people, not because of the power of copyright law. What can be accessed can be copied, so secrecy and inaccessibility is valuable. 3: American tech companies will pull out of Europe because they want to do acquisitions The incoming political administration will probably bring an end to Lina Khan’s era of the FTC, and era in which the FTC did stuff. We will go back to a ‘hands off’ policy in which big companies will acquire each other pretty often without much government interference. But, even in Khan’s era, the real nail in the coffin for one of the biggest acquisitions - Adobe’s attempt to buy Figma – was regulators from the EU and UK. Those regulators will probably keep doing stuff, so I think it’s likely that the next time some company wants to acquire a close competitor, they just close up shop in the EU, maybe with a long-term plan to return. 4: The tech industry’s ‘DEI backlash’ will run up against reality The reality is that the gap between women and men in terms of college degrees is really big: “Today, 47% of U.S. women ages 25 to 34 have a bachelor’s degree, compared with 37% of men.” And that a great deal of the tech industry’s workforce is made of up highly-skilled people who are on H-1B visas. The synthesis will be that tech workers will be more diverse, in some respects, but by stripping away the bare-bones protections around their presence, companies will keep them in a more vulnerable and exploitable position. But hard right-wingers will have plenty to complain about because these companies will continue to look less white and male, because the labor pool is not that. 5: Local-first will have a breakthrough moment I think that Zero Sync has a good chance at cracking this really hard problem. So does electric and maybe jazz, too. The gap between the dream of local-first apps and the reality has been wide, but I think projects are starting to come to grips with a few hard truths: Full decentralization is not worth it. You need to design for syncing a subset of the data, not the entire dataset. You need an approach to schema evolution and permission checking These systems are getting there. We could see a big, Figma-level application built on Zero this year that will set the standard for future web application architecture. 6: Local, small AI models will be a big deal Embedding models are cool as heck. New text-to-speech and speech-to-text models are dramatically better than what came before. Image segmentation is getting a lot better. There’s a lot of stuff that is coming out of this boom that will be able to scale down to a small model that runs on a phone, browser, or at least on our own web servers without having to call out to OpenAI or Anthropic APIs. It’ll make sense for costs, performance, and security. Candle is a really interesting effort in this area. Mini predictions Substack will re-bundle news. People are tired of subscribing to individual newsletters. Substack will introduce some ~$20/month plan that gives you access to all of the newsletters that participate in this new pricing model. TypeScript gets a zeitwork equivalent and lots of people use it. Same as how prettier brought full code formatting from TypeScript, autoloading is the kind of thing that once you have it, it’s magic. What if you could just write <SomeComponent /> in your React app and didn’t have to import it? I think this would be extremely addictive and catch on fast. Node.js will fend off its competitors. Even though Val Town is built around Deno’s magic, I’ve been very impressed that Node.js is keeping up. They’ve introduced permissions, just like Deno, and native TypeScript support, just like the upstarts. Bun and Deno will keep gaining adherents, but Node.js has a long future ahead of it. Another US city starts seriously considering congestion pricing. For all the chatter and terrible discourse around the plan, it is obviously a good idea and it will work, as it has in every other case, and inspire other cities to do the same. Stripe will IPO. They’re still killing it, but they’re killing it in an established, repeatable way that public markets will like, and will let up the pressure on the many, many people who own their stock.
More in programming
It's time for a dev blogger's favorite topic: blogging about my new blogging setup. After migrating my blog from Hexo to Eleventy, building a photography website and creating a personal one-pager, it was time to find a new digital home for all my projects. If you notice any issue after the migration, please let me know. I redirect the RSS feed to the new domain, but you might want to update the feed URL in your reader. Motivation I've been maintaining three different websites under two different names: a mostly developer-oriented blog, a photography portfolio and a one-pager gluing everything together. When I started blogging about my sabbatical journey, I was thinking about making yet another page to create a clear separation. I've decided to do the opposite and merge all my websites into one instead. As I further reduce my dependency on social media and third-party services, it has become even more important to have my own space and practice POSSE. Every decision I've made was driven by two main goals: simplicity and personality. Setup I've stopped chasing the latest and greatest. I want to keep the setup as clean as possible, which means pulling fewer dependencies. Every package adds complexity and maintainability costs, so I've only added what was necessary — no more Gulp, Sass or Tailwind. Instead of taking one of my existing websites as a starting point, I created this project from scratch. I reused some of my previous code, but I didn't want to take over any technical debt (especially with the advancements in CSS). The foundation of this website hasn't changed: Eleventy (Build Awesome). It covers the fundamentals of a static site and offers a great level of customization. I've made my project more and less dependent on Eleventy at the same time: I replaced Gulp as my build pipeline with pure Eleventy processing. I replaced most shortcodes with Markdown alternatives (and markdown-it plugins), which are better supported by editors like Obsidian. I've also replaced the Nunjucks template engine with Liquid, which comes with a few trade-offs. The main reason was to avoid creating a new collection for every set of posts I render. With Liquid, you can apply array functions like filtering or sorting in the template itself: {% assign drafts = collections.all | where: "data.isDraft" | reverse %} {% for draft in drafts %} … {% endfor %} Another advantage is having date formatting included, so I don't need another library or custom code. You can even set a default date format, so you don't have to provide it each time. What I don't like is the verbosity of the template language, although I've gotten used to it by now. Nunjucks: {{ "/tags/" + tag + "/" }} {{ modified or created or date }} {% set perspectives = ["dev", "photography", "life"] %} Liquid: {{ "/tags/" | append: tag | append: "/" }} {{ modified | default: created | default: date }} {% assign perspectives = "dev, photography, life" | split: ", " %} After migrating my existing content, I've noticed the build taking way too long, both in the dev server and the production build. I've found out that the Eleventy template cache is turned off for Liquid by default. This was useful during development, but when updating actual content, it caused every template and partial to be constantly recalculated. After turning the cache on, this issue was resolved: eleventyConfig.setLiquidOptions({ cache: true }); Check the colophon for the full tech stack overview. Perspectives The main challenge was finding a way to separate and present my main personas. The Venn diagram intersection of people interested in software development, photography and my personal life is probably small. I could use tags, but they don't offer the level of personalization I was looking for. Most personal blogs separate the content by post type (blog post, note, photo), but this doesn't fit my use case. Instead, I've decided to structure my website around three perspectives, a term I have borrowed from the Eclipse IDE. Each perspective comes with its own avatar, tagline and bio. If someone is interested in everything I share (hi Mom!), they can still view all my posts. Design Since building my photography website, I've made many changes, but I never got around to share an update. The biggest change was the design, which I have used as a blueprint for the new website. I've decided to mimic a social timeline/feed, which most people are familiar with. At the same time, it should be clear that it's a personal website and not yet another walled garden. Typography is an important part of every design. The first step is to choose between a system font stack and a custom font, which comes with the usual performance vs. personality trade-offs. I've decided to go with a uniform look across all devices. I've settled on Source Sans 3 for copy text and Hack for code. Another big change was introducing a sidebar. I always felt like the empty horizontal space was "wasted" on a desktop device, but getting the sidebar UX right is challenging. I'm happy with the result, especially with the last two panels adding a personal touch to the design. Images I've made several changes to the handling of image files. To make them compatible with Markdown editors, I had to replace my custom picture shortcode. The official Eleventy image plugin is a popular choice, but it violates my simplicity goal and doesn't support everything I needed. I've decided to go with a plain Markdown image markup and a single image file (no format fallback or 2x version for retina displays). Additionally, I wrote a markdown-it plugin to turn the optional title parameter into a caption. As for the file format, both WebP and AVIF have excellent browser support. For each image, I select the format based on the quality and file size. For my photos, I'm using WebP for the timeline thumbnails and JPG for the full-size version. Theme picker There have been some recent discussions about dark mode toggles, and I considered going for a light/dark solution based on the user's OS preference. Driven by the personality goal, I knew I had to add at least one more theme, so a proper theme picker was necessary. Similar to navigation, people like to hide the theme selection on mobile devices behind a burger menu or a dropdown. I prefer the solution by Sarah L. Fossheim, where all available themes are displayed above the page footer. A friend was scrolling through my new website, and their first question was "Why is the theme picker in the footer?". This tells me the solution was a good idea — I don't think he would have noticed a typical theme button in the header. The implementation follows standard practices: Use the light or dark theme as default based on the user's OS or browser preference. Store a selected theme in the local storage and use it as the single source of truth. Prevent a flash of inaccurate color theme. Make the theme play nicely with bfcache. I might have slightly gone overboard with my third theme, but the nostalgia kept me adding more and more features. Enjoy going back to the 90s: Feeds As browser vendors decided to remove XSLT support, I didn't bother to style my feed. Instead, I've created a new web feeds page, listing all available feeds and a short explanation. I provide one feed for every perspective and one with all my posts combined. I once offered tag-based RSS feeds. This was useful for my sabbatical, but even there I had only a single subscriber. Now, all tag feeds redirect to their main perspective feed. I've also dropped the JSON feed, despite having at least 16 subscribers. Given that every web feed reader supports Atom feeds, I don't see the value of providing a JSON alternative. A JSON feed doesn't add much complexity, but handling different media types (post, photo) and properly escaping the content is one more thing to implement and maintain. Quality assurance I've put a lot of effort into my new digital home, and I want to make sure it stays in good shape. I've included several tools, most running automatically as part of the CI pipeline: eleventyDataSchema — lint Eleventy data values like titles or dates markdownlint — format/lint Markdown files Vale — lint content (grammar, writing style, formatting) Lychee — find broken links (internal and external) Evaluatory — check accessibility and validate HTML Oxlint — lint JavaScript files Eleventy linters are another option to analyze a template's output, but I haven't found a use case for it yet. Not everything can be automated, so I also did the following manually: Test with Chrome, Firefox and Safari Test with Polypane Test with Google Lighthouse Test with a screen reader Test with JavaScript disabled Test with CSS disabled Test at 200% zoom Test with a bigger browser font size Test with custom fonts disabled Test with delayed assets Performance It's difficult to compare the performance of two separate websites with that of a combined one. Some parts got worse, e.g. bigger font files and more CSS. At the same time, I've noticed that gzip compression was turned off for anything but HTML for a few years now (I must have missed it when upgrading nginx) 🙈. One improvement was to implement on-demand assets. For example, the lite-youtube-embed CSS and JavaScript is only loaded if a <lite-youtube> element is present on the page. As before, JavaScript is fully optional (except for interactive demos in individual posts). I've been using a mix of <noscript> and CSS to hide elements that require JS (like the theme picker). I don't like the user experience of pagination, so I avoided it for years. But now that I have almost 300 posts, I've decided to finally add it. I display 30 posts per page, with images further down still being lazy-loaded for better performance. I haven't applied pagination to tag pages, as that's more complex with Eleventy. Bits and pieces Here are some other noteworthy changes: I've removed post-specific social share images and use a single WebP Open Graph image instead. I've dropped JSON-LD. Open Graph and microformats are sufficient. "Naming is hard" is a common expression in software development, but it also applies to photograph titles. I've decided to use a counter as the title, e.g. #0100. Sometimes I do come up with a title that I love, like the Castle in the Sky, so I might mention it in the description. On a photo page, you can now open the previous/next photo using the left/right arrow keys (in addition to the existing navigation buttons). It's now possible to filter photos by date, gear, or location. Check the explore page for an overview. I've learned about and used some experimental CSS features (with progressive enhancement), like scroll-triggered animations or squircle corners. I've added an AI statement on my LLM usage. I finally have a proper about page. Cool URIs don't change, but it's inevitable when moving to a new domain. All my previous URLs are redirected, except for some assets that are no longer used. I will keep the old domain, so the redirects will continue to work for a foreseeable future. I've kept my previous Easter egg and added a few more. Have a happy hunt! 🕵️♂️ Inspiration I visited hundreds of personal websites looking for inspiration. Here are a few that I kept coming back to: Emma Goto. I love Emma's approach to presenting the variety of her interests, even though I went a different route. Brennan Kenneth Brown. This website has so much to offer. It's a great example of what the open web looks like. Rex Barrett. The sentence "Collecting hobbies is my hobby." hit home — so much that I had to steal borrow it for my short life bio. Conclusion This project was mostly about merging the content of two websites into one. I knew this would take a considerable amount of time, but I still greatly underestimated the effort. In the end, it was all about the journey, not the destination. I loved fine-tuning the design and tweaking little details that probably no one will ever notice. I still have many more ideas left in my backlog. How do you like my new digital home? If you have an opinion, let me know! Reply via email
New electronica album by Warrington-Runcorn New Town Development Plan
To protect against link rot and disappearing content, I save local copies of web pages I like into my web archive. Ideally, this archive is completely self-contained. I want a local copy of each page’s HTML and all the assets it depends on (CSS, images, scripts, and so on). I don’t want my archived pages fetching resources from the original website, which could disappear at any time. Initially, I was verifying self-containedness by hand. I’d open each saved page in a web browser, and check the browser dev tools. I’d check the Console tab for errors and failed requests, and watch the Network tab for requests going out to the internet. That approach was tedious and error-prone, and I wanted something better. I now automate the process using Playwright, a testing framework that allows you to control web browsers. I open each page using Playwright, and check if it’s making external requests. Detecting external requests in Playwright When you open a page in Playwright, you can subscribe to request events which tell you when the page issues a request. In this Python snippet, we start Playwright, start a WebKit browser, and open a new page. We tell the page to append external requests to a list, and then load example.com. We see that example.com makes two requests on page load: one for https://example.com, and one for a JavaScript file. from contextlib import closing from playwright.sync_api import sync_playwright with ( sync_playwright() as p, closing(p.webkit.launch()) as browser, closing(browser.new_page()) as page, ): requests = [] page.on("request", lambda msg: requests.append(msg)) page.goto("https://example.com") print("requests:", requests) # requests: [<Request url='https://example.com/' method='GET'>, # <Request url='https://example.com/s.js' method='GET'>] In a more complex scenario, we might use Playwright to interact with the page after loading – for example, clicking a button or filling in a form, and then we could see the requests triggered by each action. Once we have a list of requests made by the page, we can inspect the url property to check if any of them were fetching from an external website. I wrote a function is_external_request to carry out this check: from playwright.sync_api import Request import urllib.parse def is_external_request(req: Request, original_url: str) -> bool: """ Check if a request has a different origin than the original URL. A request is considered internal if: - it matches the original URL exactly - it has the same scheme, host, and port as the original URL - it uses a `blob:` scheme (for example, in-memory media representations) """ if req.url == original_url: return False # Compare the scheme/host of the request to that of the original URL. org_url = urllib.parse.urlsplit(original_url) req_url = urllib.parse.urlsplit(req.url) if org_url.scheme == req_url.scheme and org_url.netloc == req_url.netloc: return False # Ignore blob: URLs, which are in-memory representation of some data, # and not an external request. # # Playwright detects blob: URLs when loading media files; for example # using the <audio> or <video> tag. if req_url.scheme == "blob": return False # Fail open: if we can't decide if a request was internal or external, # assume it was external. return True assert_page_is_self_contained(ctx, url) I’ve wrapped these two steps in a function assert_page_is_self_contained(ctx, url), which is part of my chives utility library. The function takes a Playwright Browser (or BrowserContext) and a URL, then: Opens a new page in the browser Loads the URL in the page Checks what happened when the page completes loading It throws an exception if there are any: Uncaught page exceptions or errors Console warnings or errors Non-GET requests Requests targeting external resources The function tries to trigger important network requests that might be skipped on the initial page load. For example, it evaluates the body’s offsetHeight to force a synchronous reflow, which causes the browser to request any lazily loaded images or fonts that might otherwise be missed. Here’s an example which verifies a file example.html from the working directory: from chives.browser_fixtures import assert_page_is_self_contained, file_uri from contextlib import closing from playwright.sync_api import sync_playwright with ( sync_playwright() as p, closing(p.webkit.launch()) as browser, ): assert_page_is_self_contained(browser, url=file_uri("example.html")) I originally passed a Page as the first argument, but changing the function to take a Browser or BrowserContext lets it manage its own pages. It gets a fresh page for every verification, and closes it when it’s done. (The first time I ran this, I created a new page for every verification, and never closed them – my computer ground to a crawl as my script opened hundreds of browser tabs.) If you create a new context, you can emulate different browser settings, like location, locale, and window size. Here’s an example where we use contexts to test the same page in light/dark mode: with ( sync_playwright() as p, closing(p.webkit.launch()) as browser, ): with closing(browser.new_context(color_scheme="light")) as light_ctx: assert_page_is_self_contained(light_ctx, url=file_uri("example.html")) with closing(browser.new_context(color_scheme="dark")) as dark_ctx: assert_page_is_self_contained(dark_ctx, url=file_uri("example.html")) In my web archive, I check every page in light/dark mode, and at 1×/2× pixel densities. It takes at least a second to run each verification, so I cache the results. As I save new pages, they’re added to a verification queue, and I’m warned of any missing assets. Unsurprisingly, the first run flagged many missing fonts and images, which I added to my local archive. Every saved page now passes cleanly. Browser is best-effort, but may still miss things Even with automated testing, it’s difficult to guarantee a completely self-contained archive. Different instances of a browser might load different assets, and a single page load might miss different variations: Responsive images – web pages can offer different sizes of an image, and the browser chooses a specific size based on the current window dimensions Appearance settings – a browser might load different fonts or images depending on light/dark mode Interaction-dependent assets – a web page might have assets that are only required on hover or user action You can mitigate some of this by testing with multiple contexts or adding post-load interactions, but every step makes the process take longer. Fortunately, most of the pages I save are simple, blog-style pages where I’ve stripped out the JavaScript. For my specific use case, initial load verification is more than good enough. Other uses for this code There are other projects where it could be useful to intercept and audit the requests made by a web page: Verifying your Content Security Policy (CSP). The CSP header restricts what resources can be loaded by a web page, and you could check that your configuration has the correct effect. Auditing login screens. You could check that sensitive pages don’t load any external scripts, which mitigates the risk of credential theft via a compromised third-party. Testing analytics and tracking. You could script user flows, like a shopping cart checkout, and confirm that tracking events fire correctly. I don’t do much front-end web development, but Playwright has become a key tool in my toolbox. I read a lot about people using AI agents and MCPs to interact with websites, but I prefer Playwright. It’s more fiddly and requires explicit instructions, but it’s constrained, deterministic, and I never have to worry about it doing something unexpected. [If the formatting of this post looks odd in your feed reader, visit the original article]
It’s four years since I bought a printer. I thought I would provide an update on running costs, print sales, and profit. I’m happy to be completely transparent about all this as I know it can be difficult to figure this out if you are thinking of creating your own printing business. January 11th 2026, 11:30am All the photos here are work shot in the last twelve months. Year Four Investment? Nothing other than consumables this year. I did purchase a high-spec film scanner last year, detailed here, but don’t consider that as part of this. The scanner has paid for itself several times over, and it may become a factor in future years (see below). Income Income is made up of sales through a local gallery that I am part of, along with web sales: 5,940.- CHF - sales through the gallery 80.- CHF - sales through web shop = 6,020.- CHF total income Total income is down about 50% compared to 2025. Costs I had to purchase consumables (paper and ink), at a total of 957.- CHF. That cost is included in the total here under the “print consumables” category. January 11th 2026, 3:30pm Rental cost of space in the gallery was reduced due to having overpayments of rent and my loan to the gallery, from the previous year(s), paid back to me. -1,500.- CHF - rental cost of space in the gallery -957.- CHF - print consumables (paper, ink) -1,138.- CHF - framing/mounting/postage -1,135.- CHF - commission to the gallery -300.- CHF - web shop subscription 0.- CHF - stripe / paypal fees 0.- CHF - advertising (none this year) = -5,030.- CHF total costs Total costs are down by about 60% compared to 2025. Total Profit 6,020.- CHF total income -5,030.- CHF total costs = 990.- CHF total profit A Quiet Year? Actually an average year. The previous couple of years were outliers, due to large orders, so really I’m back to normal in terms of sales. The key factors in managing to generate a [small] profit this year were: 1) no longer having to pay back the loan used to finance the printer, and 2) reduced rental cost of space in the gallery due to a previous loan to it being repaid. Also having to spend almost 1,000.- CHF on ink this year ate into profit. This is probably a “once every three years” cost given the rate I print at. The principal cost is rental of space in the gallery - in a normal year this would be 3,000.- CHF. Essentially this means, given the material cost of printing and framing/mounting and commission to the gallery, I need to make about 5,500.- CHF of sales to break even. Solar Eclipse, August 12th 2026 That’s a grind. However I did receive two substantial orders at the end of October, which haven’t made it in to this year’s figures and should boost next year’s. Maybe even a good sign for the year to come? At least now the original investment loan is repaid, the printing costs cover themselves in most years. I may look at offering scanning services now that I have a high-spec film scanner, which could become a scan + print service. More thoughts about that next time.