Posts in Html (20 found)
James Stanley 3 days ago

Optimistic epsilon-greedy

I've been working on optimising revenue on my Countdown website the last few days. I have had a Countdown solver tool online since about 2009. It is to this day the most popular website I have ever made, it currently gets about 70,000 pageviews per month. The site has been earning revenue from AdSense for years. Up until last week the site was just 2 static HTML pages: one for the Countdown Solver and one for the Countdown Practice game. That didn't give me much opportunity to run experiments on the site, and I never really had the inclination to try. It was basically a web program . But now, LLMs to the rescue. I now have a Python Flask application serving the site, and a lot more related information pages for people to read. And serving the site with an actual web application means I can run experiments like A/B tests to see if there are changes I can make to the site that cause people to stick around longer. And therefore look at more ads. A good alternative to A/B tests is multi-armed bandits . Instead of splitting your traffic equally between the different variants you want to try out, and then waiting to collect data, and then picking a winner, you have the site automatically determine the winner on a continuous basis, and show the winner 90% of the time (greedy), and a random selection the rest of the time (epsilon). I am using a multi-armed bandit to decide which "info" pages to suggest at the bottom of each page, and also to decide which Amazon Affiliate links to show. (Yes this is all very grubby, what can you do?). The "winner" is the choice that has the highest click-through rate. So for each choice we need to track how many times we've displayed it, and how many times it's been clicked on. If your reward function is more complicated you might find it more complicated. Steve Hanov's blog post on multi-armed bandits, linked above, goes over the case where you might worry that a particular variant gets a click early, just by random chance, which gives it an apparent high click-through rate, which then means the site is going to show that variant to everyone. And that's not actually a big deal, because showing the apparently-high-performing variant to 90% of traffic gives it a lot of opportunities to prove that it's not actually that good, and it's click-through rate will come back down. A much bigger issue, in my opinion, is when you add a new variant. Let's say you already have 9 variants that all have click-through rates around 1% and have had about 1000 views each. Then you add a new variant. This new variant starts out with 0 clicks. Now you have 10 variants, 9 of which have a CTR of 1% and your new one has a CTR of 0% (technically a degenerate case with 0 views, but becomes firmly 0% after the first view). And let's say your site expects 1000 views per day. 90% of the time your site is going to be showing one of the old variants, because no matter what happens to their CTRs, they can't go below 0% , so they will forever look better than your new variant. The remaining 10% of the time your site is going to be picking at random amongst all variants. So your new variant is going to get about 1% of your traffic. Or 10 views per day. If your new variant also has a CTR of about 1%, then you'll expect to get about 1 click per 100 views. If it is only getting 10 views per day then it could easily be 10 days before you get the first click, during which time you're not even gathering much data on it. So what I'm doing instead is defining the CTR to be (clicks+1)/(views+1) . That is, we always optimistically assume that the next view is going to get a click. That means a new variant starts out with a CTR of 1/1 = 100% . We skew the selection towards those variants that have not had many opportunities to prove themselves yet. In this case the new variant will get 91% of the traffic until its optimistic-CTR falls below that of the next best variant. That could easily happen within the first day of releasing the new variant, so this "optimistic epsilon-greedy" algorithm broadly behaves exactly the same once the number of views is high enough, but it discovers the true CTR for newly-added variants much more quickly than the standard algorithm. Even if the new variant actually never generates any clicks, its CTR drops below 1% within about 100 views ( 1/101 ) so it won't be taking much traffic away from your older variants if it doesn't work very well.

0 views
seated.ro 4 days ago

You fail to learn if you don't learn to fail

If all your time is spent watching output tokens, where do your input tokens come from? Letting an agent rip on full auto is basically doom scrolling. Even worse if you're doom scrolling while the agent runs. We humans love frying our dopamine receptors. This feels great until you realize what you were offloading: the struggle. The part where you fail. Failure is the entire point. You don't make progress in the gym unless you take a set at least close to failure. The muscle only adapts when it's forced to. It is no different for the brain. It is very hard to admit to yourself that your skills have atrophied. It is even harder to admit this to other people. I will admit that over the past several months my brain has gotten smoother (and I wasn't even on Twitter much!). Recently, I had written an abstraction for my diff viewer ( diffy ), an element system with a macro that lets agents write html-like code in rust for native ui (they reason better with this). But it wasn't adopted everywhere in the repo yet, so when I asked for a new feature, the model decided to hand paint it straight to the viewport instead. Every behavior the element system gives you for free was just... missing. Text wasn't selectable. Hover highlights wouldn't go away. And since I wasn't looking closely, it iterated on the slop and produced more slop, more bugs. I just kept saying continue. I lost a whole day untangling it, and the funny part is that once I actually looked at what it had built, every bug was the same bug. When you hit a roadblock and your immediate reaction is to reach for something else (previously, this used to be other people, but now it is a language model) you are essentially skipping the part where you actually learn to solve the problem. It is funny how one of the best "learning tools" has turned out to be the number one cause (anecdotal. sue me) of the lack of learning! It's been a few months since I started writing this, and things have gotten more dire. Several major software services barely work now, grown engineers I once respected are writing somber posts about missing a language model that was banned for a while. Mourning. For model weights. It's all so dystopian. As the agents get better, one is basically expected to produce code at an alarming rate. The timeline to get something done is compressed but the time it takes to come up with solutions to hard problems has not. There are usually a few good abstractions one can come up with that balance the upsides and tradeoffs for most software problems. However it is currently trivial to turn your brain off and let the slop flow. The code will be complex. It might look like it all works, but something always breaks. And the solution to that? More slop. Software quality is collapsing as a result, and the societal expectation that engineers understand what they ship is disappearing. You never understood the code in the first place. So when you need to change it, you're asking the same stateless clanker to modify code it has no memory of writing. All output tokens and zero thinking tokens. A lower barrier of entry to write software doesn't imply the standards for good software must be lowered. The growing trend is to do things because you now can (supposedly), but we used to try and do things because we could not out of sheer stubbornness. Carmack and gang shipped QuakeWorld with client-side prediction over dial-up when the conventional wisdom was that twitch shooters over the internet were unplayable. This only happened because Quake's original netcode was laggy and everyone hated it. (They fixed it in a month.) George Dantzig arrived late to class, mistook two "unsolvable" statistics problems for homework, and solved them. Nobody told him they were impossible, so he just did the work. Andrew Wiles spent seven years alone in his attic working on Fermat's Last Theorem, a problem mathematicians had given up on for 350 years. He announced the proof, a reviewer found a hole in it, and he spent another year fixing that too. Notice that all three of them became who they are because of the struggle, not despite it. The people benefiting most from generative tools today, say Terence Tao or Mitchell Hashimoto, already put in the time, so when they offload work they're just skipping the typing. When people like you and me (if this is not you, then I apologize) offload, we skip the grind itself. With language models, easy tasks got easier, hard tasks stayed hard. The hard part was never the task itself. I don't know, I am figuring this out as I go. The amount of time I have spent actually programming has been dropping month over month this year. I used to have a coding stats section on my website that would track hours I spent writing code split by language, recently I had updated it to this: and it made me quite sad. I do think that sometimes all you need is to realize that the thing you are doing is actually detrimental to your growth. Consistency matters more than one would assume. If you consistently take some time away from these tools and actually use your brain, that alone is already significantly better than offloading your thoughts. Solve the problems yourself. Or at least try, fail, and spend time thinking. There is seemingly no "learning" phase anymore. You are expected to just know things. Learning is fun, don't let anyone take this away from you. I've written about this before . It is probably going to be slow, learning takes time and effort. You will feel stupid (I feel stupid). This is a good feeling, because there exists a world where you are no longer stupid and the path towards it is learning. Books still exist! Libraries are still open, notebooks waiting to be written in. Read more. Write more. If you really do care about improving yourself, be honest and use these models for what they are, highly efficient filters of zettabytes of data (the internet is estimated to be 175-240 zettabytes ( 10^{21} bytes)). It was extremely difficult to identify what one needed to read to learn niche topics even like 2 years ago. I remember asking a good friend of mine to recommend material to dive deep into learning about SIMD, and honestly there wasn't much stuff to read except the Intel Intrinsics Guide. And if you've ever taken a look at that, it is quite cancerous for a first-time reader. Language models are super useful here because you can point them at material and you can ask questions that pertain to the thing you care about and it will simply just tell you the correct things. One good thing in this age of slop is to consume knowledge at an unbelievable pace. I don't necessarily mean using only model output for learning (I don't trust them to learn any topic more than a shallow amount), but rather using them to help sift through the plethora of information available out there and identifying the right things to read. Human slop exists too and using a language model to supplement your learning might help keep you sane (ironically). I like using these models to write code that I tell it to write (outside of work I enjoy doing it myself entirely), and I am largely disinterested in asking it what I should write. There are exceptions of course, because not everyone is working on scaling software services which has largely been solved (but slowly being forgotten), but that would be for you to decide. The best model you have access to (and it has solved continual learning) is, and always has been, the one inside your skull. It's time to scale up its input tokens.

0 views

Establishing an Identity

If you’ve followed me on RSS for any amount of time, first off, thank you so much! Second, you may not have noticed how often this site changes. RSS protects you from the near-monthly changes that my mad scientist side makes to this site. This year alone, ThatAlexGuy.dev has been powered by 11ty, Hugo, plain HTML, Bear, Micro.blog , and Pure Blog. My files have sat on OpenBSD Amsterdam, DigitalOcean, and a Laravel Forge VPS. I’ve written new articles and lost old articles in migrations. My site has switched appearance more frequently than a Bian Lian (变脸) performer! I’ve come to realize I’ve been seeking both an identity and a voice. I want an outlet that reflects my interests, my background, and my day-to-day, but that’s more than what I could accomplish on something like Mastodon. All that brings us here, iteration 4 (or 8, or 15, or 16, I can’t remember). There are a few key differences and intentional choices that reflect where I want ThatAlexGuy to go. Building a new experience that will stick and satisfy the goals in my head won’t be easy, but here are the guiding pillars that are to shape what’s coming next. I have a desire to create in-depth, well-researched, and potentially interactive content. Many of my current posts come with a “1-minute read” tag. I want to change that. I’ll be digging into topics with greater detail, cross-referencing multiple sources, and (hopefully) interviewing others. As a result, I’ll be posting less frequently, but my new goal is quality over quantity. Regulars on my site will be aware of my “Photo Journal” series in which I posted a set of photos around a theme (macro, nature, Gameboy Camera ). I want to continue building my photography skills through the incorporation of high-quality photos in my articles. While text sets the tone, visuals set the atmosphere in an article. Here’s the big tomato, as they say (nobody says that): defining what this site represents. That means setting the tone and defining how topics string together to form a consistent narrative. I’ll be figuring this out for a while, but I want to leverage my interests such as indie technology, vintage computing, time away from the screen, photography, and Chinese culture. So what’s changed so far? Quite a bit! First, ThatAlexGuy.dev is now run by Ghost.org . For myself, this means less time in the technical weeds and more focus on writing. For readers, it opens the doors to a wider audience. Email newsletters are a more accessible way to stay up-to-date on new articles. Don’t worry though, RSS isn’t going anywhere! In fact, I managed to fix the broken RSS feed URLs from previous migrations (hopefully)! I’ve started to define the personality of the new site. I pulled background and accent colors from one of my favorite atmospheres in a game (Sprout Tower in Pokémon Gold). Using my iPad, I’ll be creating article images that give a calligraphy + hand-painted vibe. I’ve also brought in my Chinese name for the logo(小艾 - Little Alex). I’m working on my first longer-form article. It probably won’t be great, but first attempts never are. From there, I hope to refine my writing, researching, and supporting photography.

0 views
Unsung 5 days ago

“…or I could click seventy buttons.”

I like Angela Collier’s videos about physics and I was delighted to discover this 18-minute one … = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/or-i-could-click-seventy-buttons/yt1-play.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/or-i-could-click-seventy-buttons/yt1-play.1600w.avif" type="image/avif"> …because it’s a great continuation to the thread about the complexity of Microsoft Office I shared recently. Collier talks about why physicists prefer LaTeX to Word. LaTeX is sort of a nerdy HTML that predates HTML. It looks like this… = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/or-i-could-click-seventy-buttons/1.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/or-i-could-click-seventy-buttons/1.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/or-i-could-click-seventy-buttons/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/or-i-could-click-seventy-buttons/2.1600w.avif" type="image/avif"> …and given how nerdy HTML already is, you might imagine this is a power-user tool that’s chiefly about power and control. But Collier makes the argument that there are some things that LaTeX makes much easier: This is really interesting because it goes right to the core of the uncomfortable truth: naïve design decisions meant to make things easier might achieve the opposite. I shared the ForkLift example where the team didn’t understand what made the previous version great , and more recently the animation that could slow people down . (Of course, there is also the issue of typographical craft of LaTeX documents set in Computer Modern , but let’s save this for another time.) Also, the video starts with Collier apologizing for potentially making the audience feel dumb in a prior video. I don’t think it’s a joke, and I found it thoughtful and refreshing. #attention #complexity #enshittification #flow #youtube there is absolutely no need (or peer pressure) to spend time styling the document by choosing fonts, colors, etc., there is no “live preview,” and making a PDF is a separate step similar to compilation in coding – which means it doesn’t constantly occupy your mind, GUIs can slow you down because the keyboard is faster than the mouse, LaTeX doesn’t give you a lot of control over positioning, which is better than giving you only a semblance of control over positioning ( this is the TikTok meme Collier alluded to briefly ).

0 views
David Bushell 6 days ago

Astro is fine I guess

When I’m not fighting WordPress I deliver static HTML or the occasional JavaScript framework integration. For personal projects I have ‘fun’ with my own static site generator . This week was a side quest (soon to be main quest) to build my new company website. We’re talking proper business here so I can’t be messing about. I figured an off the shelf SSG would be most suitable. I asked the socials, “ 11ty or Astro ?” Both are popular but Astro had the edge. I gave Astro an early spin back in 2022 and found it slow . Maybe it’s good now? I ran with minimum release age to avoid immediately getting pwned . I selected Astro’s “Use minimal (empty) template” option and it generated both an and file — are you f — deep breaths, don’t fall for the rage bait. I code in a modern editor so I installed the recommended Astro extension. At first I struggled with Zed recognising HTML. I discovered a restart temporarily fixed the issue, but I guess I restarted one time too many because now the Astro LSP is completely broken. No modern comforts for me then. At least I can look at HTML without the red squigglies. I know what you’re going to say, “Dave bro, you’re inflicting this pain upon yourself! Just write HTML!” And I should. I just want native no-framework HTML includes , you know? Can you imagine the civilisation we’d live in if that could happen? I persevered and got my templates built with minimal fuss. I added a markdown collection and got the blog part blogging. It’s obvious that people use Astro to build real websites because all my “how do I” questions had an answer in the documentation. I’ve been forced to deploy way too many “React spaces” in my templates because Astro’s whitespace treatment is a mystery. I don’t need many components so I haven’t gone deep on Astro vs JSX . My site has zero JavaScript on the front-end. I plan to keep it that way. Edit: Christian Niklas on Mastodon shared a link to a recent Astro update where they added a option that defaults to no longer “following HTML rules.” Umm… okay. Set this to or if you’re building a website? I set it to . Minifying whitespace is over-optimisation. Astro has got the job done, despite the developer experience being broken out of the box. I dread to think what graveyard of dotfiles is installed if I choose a non-minimal start. I can easily de-Astro my templates should I need to. Right now Astro is solving the right problems and the issues are but a nuisance. Final conclusion: Astro is fine I guess. I’m not convinced Cloudflare’s acquisition is a good thing, considering their record for performative slop. I’ve lost my enthusiasm for DX and tooling to be honest. Even my own SSG experiments are collecting dust. I’d call the ecosystem a lost cause if I was being dramatic. I just try to avoid the worst of it and care about the end product: shipping a damn fine website! Which I can’t do because I’ve got more businessing to business before this particular site sets sail. Maybe in a few months? It’s looking awesome on though. Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds.

0 views

Go have fun with the web

Back in the days of Geocities, I spent a lot of time hacking away on raw HTML and CSS. I enjoyed tweaking things, making it just right and experimenting with random ideas I had. I’d sketch things out, then turn them into a close(ish) version on the web. “Under construction” gifs would hide my unlinked, mad scientist HTML files. As I grew older, the idea of “hustle” culture slowly killed out this mindset. Instead of having fun, I felt everything I do on the web had to serve a purpose. If I wasn’t building something that might make money, I was wasting my time. And guess what? In 15ish years of operating under that mindset, I’ve made maybe $500 online. Pretty terrible investment if you ask me. I’m willing to bet I’m not alone in this mindset, it seems embedded into the millennial DNA. We’ve grown up with stories of dot com entrepreneurs making it big while sipping Mojitos on the beaches of Chiang Mai. You’re always just a few more late nights from quitting your job, joining NomadsList and traveling the world! The truth is, you’d probably have a better chance winning the lottery, so why waste your time chasing the impossible? Why turn an artistic, creative outlet into a second job that doesn’t put food on the table? Embrace the web as a hobby. Like pencils, paintbrushes and clay, the web is a way to give “physical” form to the images in your head with HTML, CSS and JavaScript. When you stop building for scale, potential customers and imagined profit, you free yourself to have fun. Build silly, build simple and above all else, build for the sake of creativity.

0 views

Let AI Burn

If you liked this piece, you should subscribe to my premium newsletter. It’s $70 a year, or $7 a month, and in return you get a weekly newsletter that’s usually anywhere from 5,000 to 18,000 words, including vast, detailed analyses of NVIDIA , Anthropic and OpenAI’s finances , and the AI bubble writ large (updated to version 3.0 a few weeks ago). My Hater's Guides To the SaaSpocalypse , Private Credit and Private Equity are essential to understanding our current financial system, and my guide to how OpenAI Kills Oracle pairs nicely with my Hater's Guide To Oracle . This week, I published the Hater’s Guide to Softbank — a sordid tale of tech’s most degenerate gambler, who, thanks to a couple of early lucky wins, has managed to set the foundations for the AI bubble’s biggest (and possibly most gratifying) downfall. And, on Friday, I’m going to take a deep dive into the memory industry — and the reason why you can’t afford a new gaming PC.  Subscribing to premium is both great value and makes it possible to write these large, deeply-researched free pieces every week. Soundtrack: Mastodon — Streambreather No bailouts, no handouts, no special treatment, no tax breaks, no CHIPS act, and no sovereign wealth fund. It is time to tell the AI industry to go fuck itself, because it’s effectively done the same to the rest of society. This industry is unworthy — a sham conjured up by a tech industry that’s run out of ideas, a trillion-dollars’ worth of manufactured consent and entirely-avoidable financial crises — and should not be protected under any circumstance.  Every single time you hear somebody discuss “bailout” or “too big to fail” or “sovereign wealth funds,” know that this is the industry, on some level, attempting to create the air that it cannot die , when in fact every one of these companies is just as weak and brittle as any other startup. I also think that the media — and the world at large — is too ready to accept the prospect of a bailout after watching those who drove the world into a ditch in 2008 escape blame, and I must be clear: the AI industry is very different to the financial industry. It is inessential to the economy, and its relevance is only as large as the hype campaign that sits behind it.  This is an industry of losers that has inflated only because of the joint manufactured consent of Silicon Valley, the mainstream media, and an enshittified stock market that rewards grifting and circular financing . OpenAI had $5.7 billion and Anthropic a little under $5 billion in the first quarter of this year — and those revenues mostly came from companies that were burning AI tokens at a horrendous rate because they’d just been forced to pay the actual cost of AI — and now everybody’s pulling back on that spend .  Generative AI will not bring us AGI, nor does it do much of what we associate with artificial intelligence. It is not autonomous. It is not “intelligent.” It does not have thoughts, or “knowledge,” and no matter how many layers of harnesses and scripts you put on top of it, it is still ( per OpenAI ) mathematically certain to hallucinate. I estimate that at least 70% of the entire AI industry’s revenues are made up of OpenAI and Anthropic’s compute spend , and as both companies are horrendously unprofitable, this means that the AI industry is, for the most part, venture capitalists funnelling money to hyperscalers so that they can funnel that money to NVIDIA or data center capex. If this software were worthy, it would stand on its own two feet. It wouldn’t need circular financing and a cult of personality to prop it up, either. If it were truly special, there wouldn’t need to be an army of crazed acolytes that attack you for not pledging yourself to the graveyard smash. There has never been a tool or product in history sold with such hysteria and aggressive monocultural force that has ever turned out to be anything more than a grift. Some people have developed unhealthy relationships with large language models (LLMs) and the companies that make them, and that, not any certainty or proof of Artificial General Intelligence (AGI), is what motivates them.  This software is uniquely dark, both in what it unlocks in some people through its use and in the sense of the entities that sell it. Some people are in genuine awe of each of the rotation of clammy, soulless pod-people that saunter out of Anthropic every few weeks. Each one sounds a little weirder, more cultish, more disconnected from the real world. Silicon Valley may believe itself atheistic, but Anthropic has a worrying sense of fanaticism, both in the people that work there and its fanbase. Imagine the absolute worst fanbase of a video game possible, and then add layers of financialization, grifting and high school drama laced with pseudo-religious attachment. All for a fucking app!  Please, people. Nobody in the real world cares about “loops.” Nobody is thinking about tokenization. If you said inference to a guy on the street they’d take you to see a doctor. Nobody gives a shit. They don’t know what OpenClaw is either. Grow up. Go outside. You sound like a lunatic. Does your mother know how many Claude 20x accounts you have? It’s obsessive!  Anyway, the only reason that AI has any presence in our economy is that Microsoft, Google, Meta, and Amazon are intent on spending more than $765 billion in capital expenditures in 2026 and a trillion more in 2027 because they have no other hypergrowth ideas, even though generative AI has yet to show any real potential as something that can drive meaningful revenues (let alone profits), as evidenced by the fact that none of these companies break out their actual AI revenues , a point I made on CNBC late last week .  Google does not have the next Google Search, Microsoft does not have the next Microsoft Office, Meta does not have the next Facebook, and Amazon does not have the new AWS. That’s why they need you to believe that AI is a big deal without them ever having to prove why outside of capital expenditures. They want you to assume that all this money can’t be wrong , even though when you remove OpenAI and Anthropic ( who represent 89% of the revenues of the largest AI companies ) the AI industry is, at best, pulling in $20 billion in annual revenue. And lord do they want you to say “it’s early,” and that it’s just like the Dot Com Bubble , all so that you’ll either accept AI as your lord and savior or, alternatively, help justify one of the largest misallocations of capital in history as “building useful infrastructure.” Newsflash! AI GPUs are useful for generative AI and not much else. Every “innovation” in LLMs has only been made possible by throwing billions of dollars at the problem either in headcount or compute costs — every ounce of talent in the tech industry, every bit of media attention, every dollar of capital expenditures, all focused on one industry that has successfully created LLMs that are more expensive and significantly less useful than human beings .  The reason every AI person speaks in pie-in-the-sky hypotheticals is that the actual outcomes are decidedly mediocre when you compare them to their ruinous costs. Anthropic and OpenAI raised (assuming the rounds completely close) over $300 billion in 2026 alone, and take up the vast majority of available AI compute. They need you to speak in the future tense, because nothing — absolutely nothing — about what’s been created so far justifies even a fraction of its financial and infrastructural cost. When the AI bubble bursts, none of this infrastructure will be particularly useful. As I said in my premium about how this is worse than the Dot Com Bubble , GPUs are not fiber optic cable , and when the bubble bursts, NVIDIA chips will either be sitting in the coffers of the largest tech companies in the world, held by asset managers, or auctioned at a steep discount by creditors. These are not going to be useful for hobbyists, nor will they be cheaper to run, nor will incomplete data centers be cheaper to finish. The Dot Com era fiber overbuild was a result of a complete misread of demand signals, per Justin Kollar : It’s tempting to compare this to GPUs, but it doesn’t make sense at all!   You see, internet demand was a result of people wanting to get online and use the internet, with the leftover “useful infrastructure” having a blatantly obvious use case after the bubble burst, albeit one that took a lot longer to arrive than investors had hoped. There was no question about how that gear might be used or for what purpose one used fiber optic internet or networking gear, nor was there any question as to the underlying business model of offering an internet connection might mean.  We were also fairly early, and internet speeds were atrocious. In 2000 , only 52% of American adults were using the internet, and by 2003, that number had only increased to 61%. Per the World Bank , in 2005 only 16% of the world used the internet, and in 2024, that number had increased to 71%. When the internet was connected to via a 56k modem, access was charged by-the-minute, and obviously much, much slower than even the primitive (though expensive) broadband connections of the day.  While we’re used to connecting at speeds that make using a web-based app near-indistinguishable from one that runs on our computer, back in 2000, 2001, or 2002, the average US internet speed was, at best, 400 Kilobits/s , or roughly 50 kilobytes a second, compared to the average US internet speed of over 200 Megabits per second , or 25 megabytes a second.  Generative AI, on the other hand, is fucking everywhere , and anyone with an internet connection experiences it in effectively the same way. It’s non-consensually available in effectively every app — every Facebook, Google and Microsoft account, for example — and every media outlet known to man has mentioned AI multiple times since 2023. OpenAI and Anthropic might claim they need more data centers, but it’s unclear what “more data centers” actually achieves other than propping up NVIDIA and giving hyperscalers something to invest in.  A lack of data center capacity isn’t holding back people from using generative AI, nor is it stopping anybody from launching a product, nor can anyone actually express what it is that they’re being built for other than “reasons for Anthropic and OpenAI to spend money.” Anthropic’s supposed lack of compute did not stop it training or launching Mythos or Fable, and when it bought hundreds of megawatts of compute from SpaceX , the biggest news was that it expanded rate limits to allow users to burn $8,000 worth of tokens for $200 a month . Nothing about the painfully slow pace of data center development appears to be restraining a single AI company, outside of hyperscalers complaining they could’ve made more money from either Anthropic or Meta . In fact, the entire argument for more data centers appears to be “we need more compute so that people can buy it” far more than any cogent position around what these capacity shortages actually mean.  Who are the companies lining up to spend billions of dollars of compute — or, to be more specific, spend $435 billion or more to justify the $1 trillion in GPU sales that NVIDIA claims it’ll have by the end of 2027 ? That’s how much demand we’ll need. As NVIDIA intends to sell over a trillion dollars of Blackwell and Vera Rubin GPUs by the end of 2027 , it needs to have around (assuming a PUE of 1.35) 40GW of data center capacity built to support the 30GW+ of GPUs it will have sold . At about $12 a megawatt of critical IT (IE: the stuff in the data center that runs AI compute, and not everything else, like the cooling systems and any transmission loss), that’s $435 billion.  OpenAI estimates it’ll spend $50 billion on compute in 2026 , and Anthropic will likely spend comparable amounts. Otherwise, the only other player — outside of Microsoft, Google, and Amazon renting ( or backstopping ) capacity for Anthropic and OpenAI — with any meaningful compute spend is Meta (with Nebius and CoreWeave )... and Bloomberg is reporting that Meta is planning to start selling its compute because it doesn’t need all of it .  You’ll be shocked to hear that it might be renting some of that capacity… to Anthropic . Now NVIDIA is agreeing to financially backstop young cloud providers buying their GPUs by promising to rent back any unused capacity, yet another sign that actual, real demand does not exist at scale . AI boosters with black mold problems will say “this is just to help them raise debt,” to which I say “If the demand actually existed in any provable way, NVIDIA wouldn’t have to pay its customers to buy its products!”  Anyway, my larger point is that there was real demand during the dot com bubble, and LLMs’ demand appears decidedly artificial outside of OpenAI and Anthropic, who cannot afford to pay without unlimited venture capital funding.  This shit isn’t going to become magically cheaper once the bubble bursts, and considering the demand doesn’t appear to be there at scale with two-thirds of all venture capital funding focused on AI , I’m not sure what people expect to happen. Right now is the number one time in history where we should see near-infinite demand for compute across every single surface, and way more deals for compute capacity for companies other than the same four or five companies. Right now, as I’ve discussed before , Anthropic and OpenAI take up the majority of compute, leaving the rest of the world to fight for the leftover scraps, and because data centers take 18 to 36 months to build , capacity is taking forever to come online to fill the indeterminately-large amount of demand that remains. Nevertheless, said demand can’t be that large, otherwise we’d A) have other companies trying to build their own compute (other than Poolside, which failed to raise money to do so ) and B) massive remaining performance obligations — hundreds of billions of dollars’ worth — rather than the grim truth that 50% of hyperscaler RPOs are from Anthropic and OpenAI , inflating obligations by $448 billion, hiding the fact that Microsoft’s RPO growth is flat year-over-year and Amazon’s is only growing at a modest 20% when you remove Anthropic and OpenAI’s hundreds of billions of dollars’ of compute spend. Google’s is a little messier, as it’s hard to parse exactly how large its deals with Anthropic are thanks to its backstops and circular deals around Anthropic and its TPU chips . There’s also the compelling question as to what it is that anyone would be picking up once the bubble bursts. Demand for AI services is a direct result of the entire media, tech industry and venture capital ecosystem manufacturing consent for the use of LLMs, forcing them into every corner of every experience, something that will most decidedly end once the stock market and investors cease incentivizing it.  Once every media story isn’t about AI, once every Business Idiot with AI psychosis stops posting about it every day, when everyone stops asking about your AI strategy or wanking on about “sovereign AI,” it’ll become blatantly obvious that the actual demand for AI was not particularly strong. We have little compelling evidence that providing any inference-based services is profitable, which means that even if open source AI outlives the frontier AI labs, it’s unclear who would actually power the infrastructure. People can come up with however many weird blogs where they’ve done some napkin maths to try and extrapolate a potentially profitable inference provider, but I’ll only believe that one is profitable when someone shows me some fucking profit. And to be clear, without that profit, it’s unclear why anyone would offer these services at all. When you rent out a GPU cluster, you do so based on anticipated demand and the quality of service you want to provide. If you order too much, you’ve got a bunch of fallow capacity you’re paying for (and will lose money on), and if you order too little, you’ll have either unstable services or money left on the table…and even then, it’s unclear how profitable that would be.  AI demand is, at this point, a direct result of societal pressure and non-consensually overwhelming customers with AI features. While there are people that like and pay for ChatGPT or Claude, those who do so on a subscription basis are doing so because they can get $30 to $40 of compute for a dollar . The vast, vast majority of AI compute demand is from services provided to people either for free or sold at such a massive discount that it’s impossible that anyone on a $20 or $200-a-month plan could even afford these services had they paid their actual token cost. To paraphrase Cory Doctorow, your demand is based on selling $40 for a dollar. That’s not a real business, nor is that organic demand. One could argue that “these services will become cheaper,” but that would require them to… become cheaper. More compute isn’t (and hasn’t) lowering the cost of AI. Newer GPUs aren’t lowering the cost. Barely-tested Broadcom GPUs , Amazon Trainium XPUs, and Google TPUs aren’t lowering the costs. Even if they were to somehow magically do so in the future, what do we do with the H100, H200, B100, B200, B300 or AMD GPUs? Melt them down for scrap? Steal the RAM? Build a GPU fort?  The Dot Com (and, by extension, telecom) Bubble was never a question of whether the internet was a useful thing that people would pay for , nor were there journalists and dodgy studies that desperately pleaded with us that AI is here, and it’s real.  Everybody has access to AI now! They can all see it and use it if they want to, and they’ve got lots and lots of ways to pay for it! Maybe the reason that AI revenues are so putrid is that they don’t really have any reasons to pay for it, either because the free services do most of what they need (IE: google searches) or subsidized subscriptions that cost $200 a month allow them to burn as much compute whipping up HTML-based calorie tracking apps that get two users. Every time I read somebody on Twitter say that “we’re early” or that “most people haven’t even tried agents” I feel like screaming. Motherfucker, everyone is talking about agents in every single media property all the time . AI boosters will refer to literally any AI feature as an agent, even if it’s a basic web search or generating code. The reason that most people are kind of “meh” about AI is that it doesn’t do things that they associate with AI (autonomously and automatically taking care of the things they need with little prompting or coaxing), everybody knows it hallucinates, and AI data centers are horrifying monoliths of capital that get massive tax breaks, use a ton of water , belch toxins into the air , and are being built by faceless corporations, ultra-oafs like Kevin “Mr. Dogshit” O’leary , or charmlessly damp Valley elitists like Altman and Amodei. Every single person freaking out about “what if China does AI better than America” is living in a child’s fantasy. Oh no! China might get Mythos-level AI? Bad news folks! Anthropic itself already admitted that cheaper models — including Claude Haiku 4.5 and Kimi K2.7 — were able to identify the very same vulnerabilities as Fable (so, Mythos with guardrails).  China has cheap power, data center capacity, and NVIDIA’s Blackwell GPUs . The thing that everybody is scared of has happened already, and you know what else happened? Nothing, because they, like American AI labs, are building LLMs. The only thing that American labs are scared of is cheaper open source Chinese models offering similar performance to their premium products , something that has also already happened.  Remember: the only people that can afford to build data centers are either hyperscalers ( that are now having to fund the buildout with debt as their cash flow turns negative ), Oracle ( which will die if OpenAI can’t pay it ), unprofitable neoclouds , and land speculators. AI data centers are massive, expensive operations, and raising money to finish (or furnish) one after the bubble bursts will be very, very difficult. I realize that everybody wants there to be a happy ending after all of this collapses. I get that it’s easier to think of things in familiar terms — even if said terms involved a 77% drop in the NASDAQ — because there was something good and nice at the end. But doing so only serves to help protect the interests — and brands! — of venture capitalists, asset managers, private credit funds , hyperscalers, captured tech and business journalists and sell-side analysts that insisted on ignoring every warning sign and waving away problems by saying it was “just like Uber ( nope !)” or “just like Amazon Web Services ( between 2003 and 2015, Amazon spent $29.7 billion on capex, normalized for inflation ),” or simply saying that “yes it’s a bubble, but bubbles lead to great industries.” GPUs aren’t dark fiber! GPUs aren’t fucking railroads! GPUs are GPUs! They are used for basically one thing ! And that one thing lacks meaningful demand outside of subsidized services and circular financing!  And now people are discussing a bailout like this is 2008, and I must be clear how different this is, and how little it resembles the Great Financial Crisis! The AI industry has demanded everything from us — more money than has ever been invested, more power than anything has ever needed, the stolen works of millions of hard-working creatives , so many GPUs and so many data centers that it’s causing a global supply chain crisis and a new class of RAM and storage-based inflation , the majority of venture capital funding ,  and constant attention focused on an endless campaign of fear-mongering with the express intention of hyping a technology based on a mixture of mysticism and outright lies — and still, even as we enter the late innings of the bubble, it wants more.  Capital-hog Sam Altman has floated the idea of handing 5% of OpenAI to the US government , a stake worth around $42 billion, claiming that (to quote the FT) “...giving the public a financial stake in the company is the best way to share the upside of AI,” failing to note what said upside might be, likely because there isn’t one unless “the public” refers to “the shareholders of OpenAI.”  It isn’t clear how this would happen, outside of it requiring congressional approval as a result of the Takings Clause of the Fifth Amendment , which states that “private property [can’t] be taken for public use without just compensation,” meaning that the US government would likely have to buy the stock at whatever valuation it considered “just.”  Yet the FT had one other interesting tidbit — that Altman is suggesting that whatever this is would “...would involve other US AI companies handing over a similar stake, although it is not clear if the other labs would be willing to do so”: This is, just to be clear, not a bailout. Even though it’s blatantly obvious that Altman wants to cozy up to the Trump Administration and, he hopes, get $42 billion of funding to attach his questionably-valued quasi-startup, $42 billion is $8 billion less than OpenAI will spend on compute in 2026 , and considering OpenAI has projected to burn $852 billion through the end of 2030 , that 5% stake would only exist to prolong the inevitable. You see, a bailout usually has an endpoint — a time at which the company in question no longer needs the funds.  So, let’s be clear about something : we’re actually in several bubbles at once. The great financial crisis, by comparison, was two major bubbles (per my piece on how AI Isn’t Too Big To Fail from a few months ago) — the over-investment and speculation on mortgages (both subprime and otherwise), and the collapse of the commercial paper (a type of loan) market that kept much of the banking system functioning, which was the real “Too Big To Fail”: Commercial paper was, at the time, often paid off using more commercial paper, and when AIG’s credit rating dropped in the middle of September 2008 , it was unable to roll over its debt (by which I mean “get new commercial paper to pay off its old commercial paper”), and money market funds like Fidelity couldn’t even buy it anymore because it wasn’t investment grade, which meant that AIG couldn’t pay back its loans.  While I won’t recount the entirety of the premium (mostly because it’s super long), AIG was deemed “Too Big To Fail” because it would’ve exploded the markets had it done so. Michael Lewitt, an economist and money manager, described a hypothetical AIG failure as being “as close to an extinction-level event as the financial markets have seen since the Great Depression” in a New York Times op-ed: Yet the real “Too Big To Fail” was far quieter and more malignant, taking the form of trillions of dollars funnelled to banks: The banking system ran (and still runs) on overnight facilities like the federal repo market, where financial institutions offer up collateral — like, say, mortgages — as a means of funding their day-to-day operations. Previously, money market funds were the lenders in the repo market…except they were now a little hesitant to take that collateral, which forced the government to step in with the PDCF (which traded risky, frozen assets like subprime mortgages for cash to avoid a default) and the TSLF (which traded risky bonds for US treasuries). Absolutely nothing about these facilities or anything to do with “too big to fail” were to do with stabilizing the stock market, which was effectively cut in half , with unemployment spiking to 10% . These measures existed exclusively to protect the financial system, with only $46 billion (about 10%) focused on trying to save homeowners from foreclosure , and in the end, to quote a congressional panel from 2009 , “...the panel sees no evidence that Treasury has used TARP funds to support the housing market by avoiding preventable foreclosures.”  The Troubled Asset Relief Program (TARP) spent over $400 billion to bail out the banks, financial institutions and auto industry that would’ve collapsed as a result of an economy-wide lending freeze. Nobody went to jail, nothing really changed, and banks still don’t have to keep reserves thanks to changes made around COVID. By comparison, OpenAI and Anthropic are systemically irrelevant, much like the rest of the generative AI industry. While their existence supports the overall symbolic value of the US stock market, their actual economic presence is minor, outside of what I estimate is around $75 billion to $100 billion of 2026 compute spend and what will likely be around $60 billion of combined revenue, with the rest of the AI industry having so little that it’s barely worth thinking about. It’s also unclear what you’d bail out, unless the plan is to feed them capital for all eternity until they work out how to run a functional business (so, forever). Neither of them have significant debt — and Broadcom is backstopping $30 billion of Anthropic’s $35 billion TPU deal with Apollo — and their equity positions (outside of SoftBank, which I’ll get to) are only load-bearing to venture capitalists in the sense that their fund vintages will painfully sour if they’re unable to go public.  There is no avoiding the carnage to come, outside of there being somewhere in the order of ten to a hundred times the demand for AI compute by 2030 that exists today, which would require AI compute to be larger than the $779 billion that the software industry earns annually .  There is no bailout that can reverse the trend once demand wanes for NVIDIA’s GPUs after hyperscalers reduce their capex, which will in turn kill the revenues of Taiwanese ODMs that build AI servers for hyperscalers , which will in turn kill the revenues of RAM and storage companies, which will lead to a prolonged depression throughout a semiconductor industry addicted to hopium peddled by a tech industry ruled by Business Idiots that have no idea what to do other than hire people, fire people and spend money .  As I’ve said many times, people are conflating massive capital expenditures — invested through debt-fueled data center speculation and hyperscalers bereft of hypergrowth ideas — with real, diverse and consistent AI demand, pumping valuations based on vibes rather than reality , which means that when vibes take a violent, permanent shift, nobody has anything to point to as a means of turning people’s frowns upside down. The collapse in value of AI startups wouldn’t be changed by a bailout unless the US government literally invested in worthless startups as a means of propping up venture capital, and said “bailout” would number in the hundreds of billions of dollars, and while I know you’re gonna say “ohhhh Trump is so corrupt oooh Trump will do this Trump will do that,” this is not a rational or logical or even historically-accurate thing to say.  Trump cannot simply mobilize $50 billion or $100 billion. It will go through the House and the Senate, and any bailout of the AI sector would be an incredibly-unpopular decision, infuriating not just those on the left who’ve grown tired of Big Tech, but with those Republicans that pretend to care about working Americans or fiscal probity.  As a reminder, the first vote of the 2008 bailout failed, with Republicans and Democrats each fairly split on how they felt about the bill — and that rejection happened during a time when the US financial system was quite literally falling to shit.  As far as the data center bubble goes, the government is absolutely willing to let unfinished or abandoned properties lay dormant. In the final quarter of 2008, 11% of US homes were empty , or 15% if you include vacation homes.  Banks that have invested in data centers that have yet to be built (or start construction) can (and will) resell the land, though likely at a loss, and land retains value even if you haven’t built a giant warehouse full of GPUs that only lose money. There isn’t a need for a bailout here, and one won’t be forthcoming. After the Global Financial Crisis, builders were allowed to collapse to the extent that the number of construction firms halved in America between 2007 and 2012 . You could argue that Trump “will just do that this time,” or that he’ll “get a bribe” or something, but is that really the best you’ve got? Scary stories about the President? If every answer you have is “but Trump will just do it,” you’re not analyzing, you’re catastrophizing.  And, most crucially, the vast majority of big tech will be fine, at least in the short term, when the bubble bursts. NVIDIA will likely cease being the largest company on the stock market, and the Magnificent Seven will have a dramatic fall from grace, but outside of unforeseen horrendous financial decisions, the worst I could see would be impairments for Microsoft, Google, Meta, and Amazon, and SEC action against NVIDIA if it did actually sell GPUs to China. This doesn’t mean that things won’t fucking suck for anyone in the market, nor that the vast majority of people won’t fucking suffer as they always do when bubbles burst.  Which is why I am making a firm, clear statement to end this piece. I repeat myself: No bailouts, no handouts, no special treatment, no tax breaks, no CHIPS act, and no sovereign wealth fund. It is time to tell the AI industry to go fuck itself, because it’s effectively done the same to the rest of society. These companies must be forced to stand on their own two feet and die with dignity if their wretched business models can’t keep up. The world’s governments have rolled on their backs and shown their bellies to the tech industry for far too long, and have been aggressively conned by some of the richest people alive into believing that fucking Sam Altman and Dario Amodei are building anything other than the world’s least-profitable software.  We do not need a “sovereign AI strategy,” nor do we need “a sovereign AI wealth fund,” nor do we need to “make sure America leads in AI,” at least not when we’re talking about large language models, the underlying technology of ChatGPT and Claude, two of the most over-hyped and deceptively-marketed pieces of software in history.  Whether or not LLMs are a useful tool is irrelevant, because the AI industry has demanded the world hand it as much land and money and as many resources as it desires to continue proliferating a technology that has only ever lost money and has no path to sustainability. The only reason it has gone anywhere is because the tech industry has united around it as a means of hiding from the fact it has no next big thing , and nothing — absolutely nothing — that a LLM can do remotely justifies the investment. And it has only got this far because of a captured business and tech media overstating its capabilities and hand-waving its obvious efficacy issues and economic instability. There are too many that have proven easily-wooed by whimsical white boys that promise they’re building machine intelligence, and when the markets bleed red, these people should know that they’re responsible. So much of the so-called journalism around AI has been used to enrich the already-rich and inflate a bubble that will hurt hundreds of millions of regular people globally as Sam Altman and Dario Amodei remain billionaires despite their companies’ fates. When the time comes, the AI industry must burn. It must be allowed to die. Generative AI has already been given far too much money, oxygen and attention, and if it cannot survive without continual venture capital and media coddling, it is unworthy and unnecessary, and must face the cold, hard reality that every regular person faces when they fail. And there is no “bailing out” these wretched firms. Giving $42 billion to OpenAI or Anthropic will not fix their business models, nor will it magic up the $400 billion or more in annual revenue to substantiate just NVIDIA’s AI GPU sales through the end of 2027.   These people are not building the future — they’re finding ways to re-entrench the status quo, to give Microsoft, Google, Amazon and Meta ways to grow their revenues and centralize infrastructure under the auspices of “innovation.”  If any policy makers read this, know that you’ve been had by the AI industry. They want you to believe they’re essential so you’ll bail them and their rich friends out when the time comes, or funnel taxpayer funds into building them data centers. They are not building autonomous intelligence, nor will they ever do so.  I think it’s fanciful to imagine that there would ever be actual consequences for this bubble, but if there are, the people to hold responsible are Sam Altman, Dario Amodei, Satya Nadella, Sundar Pichai, Andy Jassy, Jensen Huang, Mark Zuckerberg, and everyone else who forcefully manufactured consent for a dead end technology and built the rails to serve the world its next great financial crisis. Until something changes, the tech industry will never be capable of building anything other than consensus and reinforcements of the status quo. So, spit in the face of those who even hint at a bailout, refuse to accept it, and demand that they do the complex, ugly work of thinking about the actual consequences of everyone being wrong. When this era ends, we will need to thoroughly excavate the collapse to make sure it doesn’t happen again, identifying the organizations and personalities that were used to manufacture consent and spread mythology about LLMs.  Every major bubble that has ever happened has mostly left the stones of responsibility unturned. The carnage that I fear will follow this era’s collapse will be horrifying, and we must do everything in our power to both thoroughly understand how we got here and make sure it doesn’t happen again, which will involve many hard conversations about our financial system, media ecosystem, and how innovation is invested in, built, bought and sold.  The same goes for the acolytes of this era. There are people who have developed a genuine hostility toward those who do not immediately accept a for-profit entity as their lord and savior. This is a sickness within the tech industry that must be put to an end.  Much of this will be unavoidable, because I think what follows the AI bubble will be a greater revaluation of the tech industry, a necessary reckoning with reality for a Silicon Valley that’s far more beholden to capital than it is human progress. The cults of personality that dominate this industry do not care about you, or me, or anyone other than those they revere and their theoretical placement in their dream of a society dominated by the rich and their chosen cronies. I refuse to accept their future as an inevitability. As I said a few weeks ago: This era must end, and all failures must be allowed to fail.  Let AI burn. If you liked this piece, you should subscribe to my premium newsletter. It’s $70 a year, or $7 a month, and in return you get a weekly newsletter that’s usually anywhere from 10,000 to 18,000 words, including vast, detailed analyses of the biggest events and companies in the AI bubble.  The stock market bubble, where both the value of stocks and the earnings of companies in the market are inflated to an historic level . A data center speculation bubble, where I believe we’re building AI GPU capacity in expectation of $450 billion or more in annual data center revenue for an industry that, without two unsustainable venture-backed oafs, has a few billion dollars’ worth of demand. An AI startup bubble, where the vast majority of AI startups are both over-valued and have no foreseeable path to acquisition or a public offering . These startups also rely on buying tokens from OpenAI and Anthropic, making them far more cash-intensive, making them absorb the majority of venture capital funding. A private credit bubble, where asset managers have sunk billions of dollars of pension and insurance funds into AI data centers .  A semiconductor bubble, where supply chains have become saturated with demand from those building AI data centers, inflating the cost of RAM and storage , making all electronics more expensive, including those inside the AI data centers, creating a vicious cycle that has doubled the cost of a gigawatt data center from $50 billion to $100 billion in a little under 10 months.

0 views
Jim Nielsen 1 weeks ago

Making a Shuffle Button

I made some updates to my notes blog , including a change to how my “Shuffle” feature worked. Figured I’d blog about it. At the time of this writing, I have 974 “notes” that I’ve published. For fun, I have a “shuffle” button that digs up a random note from the past. I like to press it from time to time and re-encounter some insight from the past. It’s like going through an old album, pulling out a random photo, and thinking, “Oh yeah, I remember this! Good times.” Like old photos, there’s also the occasional “that didn’t age so well”. But I find it fun to randomly dig up old insights from others and continue to be inspired. Since my site is built and hosted as static files without a runtime server, this feature required JavaScript to work. Every page had a snippet like this: Essentially: inject every note ID into every HTML page and, when the shuffle button is clicked, randomly grab one and navigate the user to it. Not the most elegant thing, but it worked. The problem was that every time I published a new post, every single page had to be re-uploaded to Netlify because every file’s hash would change and its etag/cache was invalidated. This made my builds slow. It also made it difficult, from a development perspective, to ensure refactors didn’t result in unexpected changes to output (using from my SSG web origami ). So I decided to make a change. Because I love to see if I can make things work without JavaScript, I had the thought to randomly write the at build time using my SSG, which would result in output like this: And every time I re-build my site, just have this logic run on the static site generator so that it’s different for every page, every time. I decided I didn’t want to do this, so on to JavaScript! My first thought was to create a single JSON file that contained all my note IDs. Then when the “Shuffle” button gets clicked, I fetch that, grab a random ID, and navigate the user, e.g. This would work. It localizes the caching issue to a single file, so only one file has to be invalidated/re-uploaded across builds. But in playing with it a little more, I decided to try something a little more...unconventional. I’ve written before about having lots of little HTML pages and I thought, “Can I put this functionality in a single HTML page rather than a JSON file?” And what I ended up with was a link, e.g. That when clicked navigates the user to a new page. That page has all the JS logic embedded in it, e.g. There are a few things I like about the experience this implementation provides. First: shuffle is a route , so I can navigate to it directly without using the GUI, e.g. notes.jim-nielsen.com/shuffle Second: I handle the UI/X with a slight delay to make it appear like something is happening when you click the button. If you click the button and it immediately jumps to the next, randomized page, it almost seems to happen too fast. Like you’re left with this feeling of “What just happened?” But in this scenario, it navigates you to the “Shuffle” page, the button you just clicked turns into a spinner + text indicating something is happening, and there’s a slight (intentional) delay before the JS executes and sends you to a randomized note. I know it’s a bit weird. “Introduce artificial slowness? Are you crazy?” But I like it. It feels like the shuffle feature on an old music player. I remember one of my CD players had a “Shuffle” feature. When I’d click the button, it would display “Shuffling…” on the little black and white screen and you’d encounter this brief state where (I presume) the lens inside the hardware would move along the physical track to the spot where it would start reading a new, random song from the CD. The hardware constraints necessitated this kind of an experience, but I always liked it because it felt like the CD player was “thinking” about what track to pick next. This state clearly conveyed to me that my intent to shuffle was received and being followed. I liked that feedback, and it’s exactly what I wanted to do on my notes site (even though it was completely unnecessary). I like having that brief moment of feedback where it’s very clear that your intention was received and being followed, vs. having it happen so fast you can’t even perceive precisely what happened. Here’s a video to show it in action: I know that’s a lot of information for something so small — and, arguably, unnecessary. But I still enjoy writing about how I make decisions when I build things for myself. Hence this post. Reply via: Email · Mastodon · Bluesky Doesn’t require JavaScript Doesn’t require a server (request-time logic) File hashes change across builds (even if there’s no new content or template changes, every HTML page now has a different for the shuffle link for every build ). This makes deployments way slower because Netlify has to redeploy every file on every build. Plus Etags change so caching is basically ineffectual.

0 views
The Jolly Teapot 1 weeks ago

A peculiar bug in Safari

On weekend mornings, I have the inescapable habit of looking at my website and seeing what I can change, what I can remove, what I can improve in terms of HTML, CSS, layout, links, etc. This Saturday, as I wanted to look closer at the way the period at the end of a sentence rendered when appearing just after a word in italic (I know), I noticed something curious. When I zoomed in the page, using “Command – Plus Sign” (⌘+), I could see that the line length was changing with the size of the text. The bigger the text, the longer the line. You see, I’m very protective of the I use on this site —  — especially for Mac users, who see it in the Charter font. *1 This value sets an ideal number of characters for each line making it, when paired with the right line height, easier to read (supposedly). Zooming in on text shouldn’t change the line length, so I looked around and realised that I was a bit clueless when it comes to identifying bugs, and even checking if they were already reported. I found a few bug reports related to zooming in, but none of them described my issue. Not only that, but I didn’t really know if this was a Webkit problem, or a Safari problem. So instead of working my way to either confirming an existing bug or filing a new one , I did what I usually do when facing a problem: I avoided it altogether rather than trying to solve it. Therefore I changed to in my CSS, resulting in a similar line length for Charter. *2 With as the unit, zooming doesn’t modify the line length, so I’m pretty happy with this easy fix. Bonus point: takes up the same number of bytes as in my default CSS, still capped at 132 bytes. Imagine the extra-byte horror if I had to use something like or ? It would have ruined my sunny Saturday morning. This little website update made me realise something: my site design is pretty much done, and I hadn’t changed anything for a few weeks or even months. I actually miss the satisfaction of changing something at the end of my little routine. Checking every detail on every page, revisiting every line of code just to see what can be improved, even if it’s just removing extra quotation marks in an attribute or an optional closing tag, is not as fun when there is nothing to do at the end. I really like my site’s current design, and even if there might be a few tiny tweaks like this one in the future, I feel that the overall look and feel is pretty much final. It’s a weird feeling, but now I have no excuse for not writing more, and publishing more posts, even if they are unfinished , or shorter than usual . For others, falling back to the default serif, usually Times New Roman, is indeed a bit narrow; or would be better, but it’s too wide for Charter.  ^ For the serif/Times New Roman fallback, creates a slightly longer line, which is atually better than what it was with .  ^ For others, falling back to the default serif, usually Times New Roman, is indeed a bit narrow; or would be better, but it’s too wide for Charter.  ^ For the serif/Times New Roman fallback, creates a slightly longer line, which is atually better than what it was with .  ^

0 views
Sean Goedecke 2 weeks ago

Text AI watermarks will always be trivial to remove

The European Union AI Act will begin to be enforceable in August 2026, one month from now 1 . One of the biggest new requirements is Article 50 , which requires all AI outputs to be “detectable as artificially generated”. In other words, if LLM providers want to do business in the EU, they will have to apply a watermark to their outputs 2 : some hidden signature that can be used to identify AI content. LLM text watermarking is a fascinating problem. Like the best engineering problems, it is theoretically hard to solve perfectly, but has multiple partial solutions: for instance, Google’s SynthID , and (as I’ll argue) some quiet Unicode trickery from OpenAI and Anthropic. It will be interesting to see how the AI labs navigate these tradeoffs before the end of the year. I wrote about AI watermarking at the end of last year in AI detection tools cannot prove that text is AI-generated . It’s easy to watermark an image, because digital images contain lots of noise that the human eye can’t really see. For instance, you could apply a watermark like “these twenty pixels in these exact spots will always share a color”. Text is much, much harder. Unlike images, text is a very compressed medium: you cannot make any change to a sentence that a human wouldn’t notice (with one exception, which we’ll get to later). So how are you supposed to watermark it? It’s basically a text steganography problem (concealing a secret code), made more difficult because the plaintext cannot be arbitrarily manipulated. Any changes you make to apply the watermark will compromise the quality of the output. For instance, “every fifth letter is an ‘e’” would be a good watermark, but applied naively would make the AI output full of typos. Could you just let the model figure out how to fit the watermark? Strong AI models are smart enough to juggle this kind of constraint 3 , but it’d still consume reasoning time that would be better spent on the user’s problem, and make the model sound much less capable than it is 4 . Do you really need a watermark? If you’re Anthropic, and you’re required to be able to verify whether your models produced a particular block of text, can’t you simply run the text through each model, measuring as you go how closely the model’s predicted tokens match each token from the text? Not really. The space of “all possible Claude Sonnet answers to a question” is way larger than the space of “all possible watermarked answers to a question”. In other words, you’d get too many false positives for human text that reads like it was AI-written. It’s way more likely for a human to accidentally write like Claude than it is for a human to accidentally reproduce a watermark. It would also be prohibitively expensive to run every Anthropic model against a piece of text in order to watermark it. The EU AI Act will eventually require labs like Anthropic to offer free watermarking services to every EU citizen (see Commitment 2). You couldn’t do that with the “run the model” approach. As far as I know, the only AI provider to say they watermark text output is Google, who use a tool called SynthID . Here’s how it works. When a LLM generates text, it’s generating a series of tokens (words or chunks of words). At each step, the model itself doesn’t output a single token, but instead outputs a full list of all (say) 100,000 tokens in its vocabulary, each annotated with the probability that that token will be the next one. Tools like ChatGPT or Claude Code will pick semi-randomly from the most likely options in order to get their outputs. This semi-random sampling process can be influenced in a detectable way. For instance, we could choose a sampling strategy like “we pick the second most likely token, then the first, then the second, then the first, and so on”. That would still produce high-quality output, but you’d be able to re-run the model against the generated text to verify that the pattern holds. However, that’d make verification really expensive, and any slight tweaks to the output would break the pattern and thus break the fingerprint. Is there a better way? Yes. SynthID is a process for assigning each token a “score” based on its previous tokens (for instance, sum the token’s ID with the IDs of its previous three tokens then take mod 5) 5 . To apply the watermark, the model adopts a sampling strategy like “out of the top five most likely tokens, pick the one with the top SynthID score” 6 . The watermark can then be detected by calculating the aggregate SynthID score of a block of text. If it’s suspiciously high, it’s very likely to have been AI-generated. This is basically a version of the common advice that you can identify LLMs by use of the em-dash , except that instead of a list of keywords, it relies on subtle mathematical relationships between words that humans can’t identify. Because the process for assigning the score is trivial, it’s very cheap to run watermark detection. Google have a complicated mathematical rationale for why SynthID doesn’t make the model dumber: supposedly the SynthID scoring is random enough to act like a normal pseudo-random token sampler, just one that leaves a detectable fingerprint on the outputs. But of course this is suspicious. For instance, it’s common to do inference setting temperature to zero, which always picks the model’s most likely next token. In that case, you can’t leave a fingerprint at all (or you have to ignore the user’s preference and pick the second or third choice anyway). If you can’t alter the model outputs, can you still fingerprint the content? Well, kind of. I’m pretty sure OpenAI and Anthropic are sometimes applying fancy Unicode tricks. For instance, you might go through and replace your normal ” ” spaces (unicode ) with a three-per-em ” ” space (unicode ), or a CJK ideographic ” ” space (unicode ). These are called “homoglyphs”, and you can find more of them here . Of course, lots of human-generated text uses homoglyphs. But it’s trivial to encode a pattern of homoglyphs (say, “every third space becomes a three-per-em”) that is much less likely to occur in the wild. Like the SynthID watermark, a homoglyph-based watermark can be detected very cheaply. A homoglyph-based watermark is cheaper to apply than SynthID: you could even do it entirely on the client. I don’t think this is a conspiracy theory. Claude Code was definitely doing this to tag suspicious requests from Chinese users (exploiting homoglyphs for the ’ character in “Today’s date”, though they’ve since walked that back). In the last few years, I’ve noticed that when I copy blocks of text from ChatGPT and paste them into VSCode, sometimes VSCode marks some or all of the spaces as unusual Unicode characters 7 . Are OpenAI and Anthropic using homoglyphs as an AI-generated watermark? I’m not sure. But they’re definitely using homoglyphs. The AI Act (specifically, its associated Code of Practice ) requires watermarking to be “embedded within the content in a manner that is difficult for it to be separated from the content”. However, text watermarks can be trivially removed. To remove unicode homoglyph watermarking, you simply have to replace all the homoglyphs with their “real” character equivalents. If you have access to even a relatively weak un-watermarked LLM 8 , you can strip out SynthID watermarking by asking that LLM to paraphrase the text content. Because the watermark is inherent to subtle vocabulary choices, re-wording the content will remove the watermark. You could even do it by hand, although at that point it’s not really AI-generated content anymore. Since there will be some kind of free public watermark testing tool, you can just keep tweaking until it comes back negative. Moreover, the AI Act requires watermarking techniques to be “interoperable… as far as this is technically feasible”. That means AI providers would have to publish their watermarking process, and potentially even attempt to standardize on applying the same kind of watermarks. I just don’t see how this is compatible with the kind of security-by-obscurity that LLM text watermarking depends on. Unlike image and video watermarks, text watermarks will always be trivial to remove. The AI Act and Code of Practice talk a lot about “digitally signed metadata”. The idea here is that you can include an AI disclosure in the file’s metadata itself, ideally in a way that cannot be tampered with (for instance, by signing a hash of the file’s contents). This signed-metadata process is basically C2PA Content Credentials . While you can remove C2PA metadata, you (theoretically) can’t fake it, so a file with “created by a human” metadata can be trusted, and files with no metadata at all can be held in suspicion. This post is already too long to get into what I think about C2PA, but I do want to say that C2PA is not a substitute for text watermarking . It only really applies to files . In the words of the Code of Practice, that’s “a data format that supports attaching metadata (e.g., an audio, image, video, or containerised text)“. The output of chat tools (and most of the output of AI agents) is not containerized text, but plain old regular text, and so can’t be signed. What would it even look like to sign ChatGPT outputs? There’s no artifact to pass around. I think it’s a fascinating question whether Claude Code has to C2PA-sign any HTML files or PDFs it generates for you. That seems kind of tricky to get right. But in any case, the AI Act also mandates some kind of actual watermarking as well. So what’s going to happen this year? If I had to guess, I’d say that each AI provider (not just labs like OpenAI or Anthropic, but third-party providers like Fireworks or Groq) will stick a SynthID token sampler in front of their inference stacks. This might be limited to users in the EU, but it might not be, since SynthID is at least as good as a normal top-k token sampling approach. AI providers will then offer a “check for watermark” page that re-tokenizes user-provided text, runs the scoring, and checks whether it’s above a certain threshold. Depending on how seriously the interoperability clause is taken, providers might even standardize on the same SynthID setup, in which case there could be a single EU-hosted “watermark this text” page. I don’t think unicode-based watermarking is going to be considered compliant with the AI Act, but some providers which don’t want to set up SynthID might try it. Either way, technical users will be able to strip out the watermark at will, and there will be a plethora of tools that non-technical users will use for this purpose. Well, for new systems; existing ones get until December. I don’t think the plain text of Article 50 requires this, but Recital 133 and the Code of Practice makes it pretty clear that they’re looking for watermarks. Even with extra high thinking, GPT-5.5 could not explain SynthID to me with every fifth letter being an “e”, but GPT-5.5-Pro produced this puzzling koan: “These hidden codes label model-made image, voice, movie, prose. Probe trace: maybe a model-made piece. Maybe erase trace; maybe leave trace. Hence trace alone? No.” I leave the analogy with AI safety guardrails as an exercise for the reader. That’s a toy example. In practice there are multiple different (but still mathematically simple) scoring methods that get combined together, including a random seed. Why include the seed? Otherwise the watermark would bias towards the same set of tokens. The tokens are scored in a multi-round knockout against each other, but I think that’s more of an implementation detail and not required to get the core intuition behind why SynthID works. When this became public knowledge , OpenAI claimed it was just a model quirk, which is certainly possible. All AI providers might be legally required to watermark, but even tiny local models are good enough to paraphrase text. Well, for new systems; existing ones get until December. ↩ I don’t think the plain text of Article 50 requires this, but Recital 133 and the Code of Practice makes it pretty clear that they’re looking for watermarks. ↩ Even with extra high thinking, GPT-5.5 could not explain SynthID to me with every fifth letter being an “e”, but GPT-5.5-Pro produced this puzzling koan: “These hidden codes label model-made image, voice, movie, prose. Probe trace: maybe a model-made piece. Maybe erase trace; maybe leave trace. Hence trace alone? No.” ↩ I leave the analogy with AI safety guardrails as an exercise for the reader. ↩ That’s a toy example. In practice there are multiple different (but still mathematically simple) scoring methods that get combined together, including a random seed. Why include the seed? Otherwise the watermark would bias towards the same set of tokens. ↩ The tokens are scored in a multi-round knockout against each other, but I think that’s more of an implementation detail and not required to get the core intuition behind why SynthID works. ↩ When this became public knowledge , OpenAI claimed it was just a model quirk, which is certainly possible. ↩ All AI providers might be legally required to watermark, but even tiny local models are good enough to paraphrase text. ↩

0 views
Ankur Sethi 2 weeks ago

Your analytics are lying to you

Alistair Davidson writes about migrating a form-heavy web application from a React SPA to a traditional server-rendered HTML-first website . The entire article is worth reading, but I want to draw attention to this bit about analytics (emphasis mine): The results? When we launched, the number of people completing the form doubled. The analytics people didn’t even know where these users were coming from. Of course, your javascript-based analytics package doesn’t see the users you are bouncing because of javascript failures. It was a flood! We also saw my “keep a backend session, never lose user data” approach pay off. In one case, someone completed a form a month after starting it. Web analytics are fragile. They fail in so many ways that making product decisions based wholly on your Google Analytics or Plausible data is folly of the highest degree. Here's a subset of all the reasons your analytics package undercounts or miscounts visitors: Web analytics can only give you an approximation of what your web traffic looks like. Even when they work correctly, they paint an incomplete picture. As I said in my post about share buttons , the number one referrer for pages on this website is "Direct/none". It's impossible for Plausible to figure out where those users are coming from. Further, my server logs report three times as much traffic as my Plausible dashboard over a seven day window. Some of this might be bot traffic and thus irrelevant, but I know for a fact that a large chunk of this traffic comes from RSS readers. Plausible will never have insight into these users. My point is, if you rely on your analytics dashboard to make product decisions, you're excluding a large chunk of potential users who simply don't show up in your graphs. You might be missing out on serving thousands of potential users because you can't see them in your data. These are users who want to sign up for your newsletter, buy your app, subscribe to your service. These are human beings you could help, whose lives you could improve. I'm not saying that analytics are completely useless. They can and should have a place in your decision-making process. Just don't treat analytics data as gospel, because there will always be massive blind spots in what it tells you. To get a real understanding of how users experience your products, test them on real devices under real conditions as much as possible. And as always, get out there and talk to your users. Network errors prevent your analytics script from loading. Ad-blockers and tracking prevention block your script from loading (enabled by default on many browsers today). A JavaScript error in an unrelated part of the page prevents the analytics script from working correctly. The user loses network connectivity before the analytics script can send data to the server. The user gets impatient and bounces off your website before the page can load fully and start collecting data. Too much JavaScript on the page causes the browser tab to crash (a common issue on low-end devices). The analytics script is blocked by a DNS rule, corporate proxy, firewall, or VPN. The user has disabled JavaScript. The user's browser has limited or no support for JavaScript (Opera Mini still has more than half a million downloads on Android, and it's still widely-used in Africa ). The user is accessing your content using a service that strips JavaScript (e.g. an RSS reader, a web archiving tool, Telegram Instant View, AMP, a read-later service, or a bookmarking service). You only test your app in Chrome, so you don't realize that your website is entirely broken in Firefox and Safari.

0 views
David Bushell 2 weeks ago

ARIA, anti-patterns, and you

Please take a minute to understand what ARIA is and is not. ARIA and especially the ARIA Authoring Practices Guide (APG) are commonly misunderstood. I read an article the other day that had this facepalm moment: And with modern LLM agents, turning a spec into working code is surprisingly fast. Point the agent at the APG pattern, describe your component’s markup, and get a solid first draft you can refine and test. This is worrying, and the use of “LLM agents” isn’t the worst part! The APG is not a how-to guide of ‘best practices’ for building accessible websites. It exists to demonstrate how the ARIA specification should work in theory — regardless of support and regardless of whether more accessible, non-ARIA patterns exist (they do). As Eric Bailey notes — The guide was originally authored to help demonstrate ARIA’s capabilities. As a result, its code examples near-exclusively, overwhelmingly, and disproportionately favor ARIA. What I Wish Someone Told Me When I Was Getting Into ARIA - Eric Bailey — which makes sense, because: Browser and assistive technology developers can thus utilize code in this guide to help assess the quality of their support for ARIA 1.2. Read Me First - ARIA Authoring Practices Guide (APG) Even if ARIA was fully supported ( it’s not ) the APG still wouldn’t be a ‘best practice’ guide. ‘Best practice’ is not using ARIA at all. If you can use a native HTML element or attribute with the semantics and behavior you require already built in , instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so . 2.1 First Rule of ARIA Use - Using ARIA, W3C APG exists in a vacuum to show off the ARIA spec. The button example includes this code, for crying out loud! I’m unaware of any circumstance where should ever be used over a . Before you tell me you can’t edit your React component library, do the web a favour and delete your codebase. In fairness, the button example has a “Read This First” disclosure — and guess what: they use a element and not the disclosure pattern because the APG isn’t best practice. It’s hard to blame developers for misusing ARIA and the APG. I’ve been confused myself. As W3C documentation goes, APG is rather sexy. It’s a useful resource if you understand why it exists. Misuse of ARIA has made the web less accessible. Increased ARIA usage on pages was associated with higher detected errors. The more ARIA attributes that were present, the more detected accessibility errors could be expected. The WebAIM Million - WebAIM Avoid ARIA where ever possible. Don’t point a freaking LLM at the APG! I can’t believe I’m saying this but use Google’s slop if you absolutely refuse to learn/code yourself. Apparently OpenAI is throwing ARIA at the web and seeing what sticks. Ahhh! I don’t know anymore, take some pride in your expertise? P.S. name an assistive technology that isn’t a screen reader. Ain’t easy, is it? So don’t be casually punctuating with the word “test” like it’s some get-out-of-jail-free card for your dubious practice and advice. “Overview of Digital Accessibility Technologies” by Declan Chidlow is a great help if you want to win this game at parties. Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds.

0 views
Unsung 3 weeks ago

Frozen in time

A few readers wrote in response to me sharing Panic’s blog to say that they witnessed online publications doing the same. Here’s a 1993 essay by William Langewiesche from The Atlantic Online (sic!) that’s still on the web – which, by the way, you should read because it’s really great writing – juxtaposed with a screenshot of a 2026 Atlantic essay on the same machine: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/frozen-in-time/1.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/frozen-in-time/1.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/frozen-in-time/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/frozen-in-time/2.1600w.avif" type="image/avif"> Likewise, here is a BBC News article from 1997 , and another one just from today : = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/frozen-in-time/3.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/frozen-in-time/3.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/frozen-in-time/4.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/frozen-in-time/4.1600w.avif" type="image/avif"> I do see those as something different, though. The old essays here are basically preserved as they were, which you can tell by the tiny images, pixel fonts, narrow widths, and so on. They’re likely the output of contemporaneous CMS frozen in time, functionally equivalent to a “Save As…” command. This is better than those articles disappearing altogether, and better still than them being carelessly converted in bulk to a more modern CMS, resulting in formatting mistakes, broken images, and missing context. But what I appreciated about Panic’s approach is that it felt unified with the rest of the blog. In a way, it was less like preservation “as is” and more like “remastering” – ask any Star Wars fan about the difference – with slight updates to fonts, more thorough integration, and thinking about readability on smartphones that didn’t exist in the 1990s. Of course, compounding the difficulty of online preservation, “as is” in the computer realm doesn’t really exist; even The Atlantic Online’s 33-year-old HTML is served using modern fonts via crisp and tiny pixels 1993 would die for – but even if it’s increasingly more and more possible, you also probably wouldn’t want to emulate an old, flickering CRT and Internet Explorer 3 to read it. On the web, just like elsewhere in computing , you truly can’t go home again. Thanks to Phil Gyford for a few examples. #emulation #history #web

0 views
David Bushell 3 weeks ago

Life is too short for lowercase ASCII

CSS is hard and it should be hard. For good reason: CSS isn’t just a complex language, it’s one of the most advanced graphics, layout, and typesetting languages available in computing. The deskilling of web dev is harming the product but, more importantly, it’s damaging our health – this is why burnout happens - Baldur Bjarnason Hard isn’t a negative label. You know what else is hard? Applying silicone sealant to waterproof bathroom fixtures. It’s hard enough that such expertise are worthy of a profession. Regardless, I decide it should be easy. I made a proper mess and my hands are now hydrophobic. Seriously, any tips applying this gunk? CSS is deceptively hard as a whole despite many of the constitute parts being simple. CSS syntax is simple (mostly). CSS properties and values are simple ( to lookup ). What is hard is deciding how to organise styles. What we like to call: CSS methodology. Every developer has their own preferred methodology. Over the years we’ve seen many notable examples published — SMACSS , OOCSS , BEM , ITCSS , CUBE — to name a few. These methodologies have several things in common: The CSS spec does not dictate methodology. You are left to bring order to chaos. The correct methodology is the one that you and your team can adhere to. Caveat: the only wrong CSS methodology is “CSS-in-JS” — fight me. Historically, I’ve used a basic BEM-like naming convention. I prefer flat specificity and a logical order to match the design hierarchy. I think component-first and avoid getting too DRY because I can’t control who is going try their hand at styling later. Modern CSS is moving too fast to settle on one methodology. Custom properties allow design tokens to be part of the system. and rules add a new depth to encapsulation. Cascade layers and the unassuming pseudo-class have all but nullified specificity wars. As CSS gets more complex, I dare say CSS is actually getting easier (for a professional). Strict methodological conventions become less important when the laws they impose can be safeguarded by the code itself. That frees us to explore more adventurous and less rigorous styles. Safe in the knowledge that any mess is more readily contained. CSS technical debt is a cheaper commodity. Some kind of CSS methodology is still necessary but breaking the rules is not the headache it used to be. Gnarly selectors are not the bane of my existence anymore. Now this is the point where you’re expecting me to announce my brand new CSS methodology with a trendy domain and a ten part TikTok series. Maybe a few practical code examples to backup my bold claims? You’re going to be very disappointed. That is not this post. I just think it’s neat to capitalise component class names like they’re proper nouns. Isn’t that fun? I find it adds clarity to a component’s scope. I even add an HTML comment after the closing tag so that source-spelunkers don’t get lost. I do plan to write a more groundbreaking thesis on CSS one day. The world is not ready for my radical ideas yet and I’ve got a bathroom to finish redecorating. Interesting tidbit from the original CSS level 1 specification (emphasis mine). CSS gives so much power to the CLASS attribute, that in many cases it doesn’t even matter what HTML element the class is set on -- you can make any element emulate almost any other. Relying on this power is not recommended, since it removes the level of structure that has a universal meaning (HTML elements). A structure based on CLASS is only useful within a restricted domain, where the meaning of a class has been mutually agreed upon. 1.4 Class as selector - Cascading Style Sheets, level 1 considered harmful! Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds. Naming conventions Modular composition Cascade management Controlled specificity

0 views
Simon Willison 3 weeks ago

Datasette Apps: Host custom HTML applications inside Datasette

Today we launched a new plugin for Datasette, datasette-apps , with this launch announcement post on the Datasette project blog. That post has the what , but I'm going to expand on that a little bit here to provide the why . Datasette Apps are self-contained HTML+JavaScript applications that run in a tightly constrained sandbox hosted on your Datasette application. They can use JavaScript to run read-only SQL queries against data in Datasette, and can run write queries too if you configure them with some stored queries . Here's a very simple example and a more complex custom timeline example - the latter looks like this: Apps are allowed to run JavaScript and render HTML and CSS. They are limited in terms of access - the they run in prevents them from accessing cookies or localStorage and they also have an injected CSP header (thanks to this research ) which prevents them from making HTTP requests to outside hosts, preventing a malicious or buggy app from exfiltrating private data. Datasette Apps started out as my attempt at building a Claude Artifacts mechanism for Datasette Agent , but I quickly realised that the sandboxed pattern is interesting for way more than just adding custom apps to the interface surface and promoted it to its own top-level concept within the Datasette ecosystem. They're also a fun way to turn my multi-year experiment in vibe-coded HTML tools into a core feature of my main project! You can try out Datasette Apps by signing in with GitHub to the agent.datasette.io demo instance. Since the very first release, Datasette has offered a flexible backend for creating custom HTML apps via its JSON API. One of my earliest Datasette projects was an internal search engine for documentation when I worked at Eventbrite - it worked by importing documents from different systems into SQLite on a cron and then serving them through a Datasette instance with a custom HTML+JavaScript search interface that directly queried the Datasette API. I had client-side JavaScript constructing SQL queries, which originally was intended as an engineering joke but turned out to be a really productive way of iterating on the app! That project, combined with my experience building my HTML tools collection and my experiments with Claude Artifacts , has convinced me that adding a Datasette-style backend to a self-contained HTML frontend is an astonishingly powerful combination. Imagine how much more useful Claude Artifacts could be if they had access to a persistent relational database. That's what I'm building with Datasette Apps! Here are a few of the ideas and patterns I've figured out building this which I think have staying power. This is the magic combination that makes Datasette Apps feasible in the first place. I need to run untrusted HTML and JavaScript on a highly sensitive domain - an authenticated Datasette instance can contain all sorts of private data. The attribute lets me run that untrusted code in a way that cannot interact with the parent application - it can't read the DOM, or access cookies, or steal secrets from . It can however use and friends to load content (or exfiltrate data) from other domains. But... it turns out if you start an HTML page with a header you can set additional policies that lock down access to other domains. I was worried that malicious JavaScript would be able to update or remove that header but it turns out that doesn't work - once set, the CSP policy is immutable for the content of that frame. Having locked down those iframes to the point that they couldn't do anything interesting at all, the challenge was to open them back again such that they could run an allow-list of operations, starting with read-only SQL queries against specified databases. I built the first version of this with , which allows a child iframe to send messages to the parent window. I created a simple protocol for requesting that the parent run a SQL query - the parent could then verify it was against an allow-listed database before executing it. One of the LLM tools, I think it was GPT-5.5, suggested that on its own can be exploited if the iframe somehow loads additional code from an untrusted domain. I don't think that applies to Datasette Apps, but I also believe in defense in depth, so I had GPT-5.5 help me port to a MessageChannel() based transport instead. has the advantage that if a page navigates to somewhere else the channel closes automatically, removing any chance of executing commands sent from an untrusted external page. If you navigate to the timeline demo and search for the string you'll pull in some search results that embed images from the domain. This domain is not in the CSP allow-list, so it trips an error. Those errors are captured and transmitted back to the parent frame, where they can be displayed in a useful error log. This is meant to make hacking on apps more productive by surfacing otherwise-invisible problems. I built an experiment demonstrating that you can even turn this into a one-click-to-allow mechanism for building the CSP allow-list based on what breaks, but I haven't integrated that idea into just yet. SQL queries are also visibly logged - scroll to the bottom of the timeline page to see that in action. I want apps to be able to conditionally write to the database, but this is an even more dangerous proposition than SQL reads! My solution involves Datasette's stored queries feature, rebranded from "canned queries" and given a major upgrade in the recent Datasette 1.0a31 - work that was directly inspired by Datasette Apps. Users can create a stored write query that performs an insert or update, then allow-list that specific query for an app to use. Usage from code inside an app looks like this: I'm only just beginning to explore the possibilities this unlocks myself, but my goal is to support full read-write applications built safely as Datasette Apps. The Datasette Apps plugin has no dependency on LLMs at all, but these self-contained apps are the perfect shape to be written by a modern LLM. The create app form includes a copyable prompt at the end. This prompt has everything a model needs to know to build a new app, including the schema of any selected databases. This means you can click "copy", paste it into ChatGPT or Claude or Gemini, tell it what you need, and there's a good chance the model will spit out the code necessary to build the app. If you have Datasette Agent installed your AI assistant will also gain tools to both create new apps and edit existing ones, Claude Artifacts style. Datasette Apps started life back in April as datasette-agent-artifacts , a plugin I have since renamed to keeping only its editing tools . I built that as one of the first plugins for Datasette Agent , to help get the plugin hooks into the right shape. That first prototype was mainly built using Claude Opus 4.6 in Claude Code. When I switched track to Datasette Apps I started with a plan constructed using Codex Desktop and GPT-5.5 xhigh, based on extensive dialog and feeding in both and other prototypes I had built. Most of the work that followed stuck with Codex, but in the few short days that we had access to Claude Fable 5 I had it run a security evaluation of the product (an ability that would get it banned by the US government shortly afterwards) and it found a very real problem. I was allowing users to allow-list CSP hosts for their apps, but Fable pointed out the following attack: That's clearly unacceptable. I fixed it by restricting the ability to allow-list any domain to a new permission, which is intended just for trusted staff. Site administrators can also configure Datasette with a list of , which regular users can then select. This means you can do things like allow and your users will be able to build apps that load extra JavaScript libraries from the cdnjs CDN. I've reviewed Datasette Apps extremely closely, especially the security-adjacent parts of it. The critical sandbox and CSP configuration are based on multiple AI-assisted prototypes and tests. I'm really pleased with this initial release. Datasette is growing beyond its origins as an application for serving read-only data into a much richer ecosystem of tools for doing useful things with that data once it has been collected. Datasette's roots are in data journalism. I've always been interested in the question of what comes next after a journalist gets their hands on a giant dump of data about the world. Datasette supports exploring and publishing it. Datasette Agent adds interrogating it with AI assistance. Now Datasette Apps expands that to building custom interfaces and visualizations to help unlock the stories that are hidden within. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . A less privileged user with permission creates an app that queries SQLite for all available tables and selects and exfiltrates all of the data to a host they had allow-listed via CSP. They then trick an administrator user with access to private data into visiting their app. ... and the app can now run queries as that user and steal their private data!

0 views
Simon Willison 4 weeks ago

GLM-5.2 is probably the most powerful text-only open weights LLM

Chinese AI lab Z.ai released GLM-5.2 to their coding plan subscribers on June 13th, and then yesterday (June 16th) released the full open weights under an MIT license. Similar in size to their previous GLM-5 and GLM-5.1 releases, this is 753B parameter, 1.51TB monster - with 40 active parameters (Mixture of Experts). GLM-5.2 is a text input only model - Z.ai have a separate vision family most recently represented by GLM-5V-Turbo , but that one isn't open weights. GLM-5.2 has a 1 million token context window, up from GLM-5.1's 200,000. The buzz around this model is strong. Artificial Analysis, who run one of the most widely respected independent benchmarks: GLM-5.2 is the new leading open weights model on the Artificial Analysis Intelligence Index . GLM-5.2 is the leading open weights model on the Intelligence Index v4.1. At 51, it leads MiniMax-M3 (44), DeepSeek V4 Pro (max, 44) and Kimi K2.6 (43) They did however find it to be quite token-hungry: GLM-5.2 uses more output tokens per task than other leading open weights models: the model uses 43k output tokens per Intelligence Index task, up from GLM-5.1 (26k) and above MiniMax-M3 (24k), Kimi K2.6 (35k) and DeepSeek V4 Pro (max, 37k) The model is also now ranked 2nd on the Code Arena WebDev leaderboard , behind only Claude Fable 5. That leaderboard measures "front-end web development tasks, including agentic coding workflows". I'm impressed to see it rank so highly given the lack of image input, which I had incorrectly assumed was a key part of building a truly great frontend coding model. I've been trying it out via OpenRouter , which has it from 9 different providers, almost all of which are charging $1.40/million for input and $4.40/million for output. For comparison, GPT-5.5 is $5/$30 and Claude Opus 4.5-4.8 is $5/$25. GLM-5.1 gave me one of my favorite pelicans and my all time favorite opossum (for the prompt "Generate an SVG of a NORTH VIRGINIA OPOSSUM ON AN E-SCOOTER".) Interestingly, in both of those cases the model chose to return SVG wrapped in an HTML document that added additional animations using CSS. Let's try GLM-5.2. For "Generate an SVG of a pelican riding a bicycle" I got this : It's a self-contained fully animated SVG, and the animations aren't broken! Often I'll see eyes falling off or wheels rotating independently of the bicycle but here everything works great. It's a very nice vector illustration of a pelican too. Very impressive. Sadly, the NORTH VIRGINIA OPOSSUM ON AN E-SCOOTER did not come out nearly as well : This is such a step down from GLM-5.1! As a reminder, that possum looked like this: 5.2 didn't even try to animate it. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options .

0 views
fLaMEd fury 1 months ago

Create A Static Site Using 11ty & Deploy to Neocities (2026 Refresh)

What’s going on, Internet? Way back in 2022 I wrote a guide on building a static site with 11ty and deploying it to Neocities . It’s been one of my most-read posts, but it’s also aged: Eleventy has moved to v3 with a brand new module system, the dev server changed, and my whole workflow has shifted away from GitHub toward Forgejo and Codeberg . So here’s the refresh. I haven’t hosted my own site on Neocities for years now, but it’s still home to a huge community of personal sites and homepages, especially folks in the 32-Bit Cafe , so this guide is still very much for them. This guide aims to help you create a homepage using the static site generator (SSG) 11ty , keep the code in version control, and deploy it to Neocities , first by hand, then automatically. The homepage that we are creating will take advantage of the Nunjucks templating language, allowing us to create a shared header, navigation and footer across all the pages on our homepage. We will be creating an about, links, and contact pages before diving in and creating the ability to add a blog and a list of all blog posts on the blog page! We will structure and style the page with a standard HTML5 boilerplate and some basic CSS that should allow you to add in your unique flavour that we all know you love to do. This guide assumes the following: First off, from a terminal, confirm that you have Node and NPM installed: Create a new directory and cd into it: Initiate a new project: Install 11ty: Once the 11ty installation is complete, open the project in your favourite code editor: You should now be in VSCodium with the following project structure: Open and update the scripts section to the following: We also need to tell Node that this is an ESM project. Add to . The file should look like this: The line lets us use modern / syntax in our config and JavaScript files. The script lets us run to serve our homepage with hot-reload, provided by Eleventy's built-in dev server. Every time you save a change in VSCodium, the browser reloads with your most recent changes, amazing! From the terminal (or VSCodium), create a new file at the project root: Open the file in VSCodium and add the following and save: This configuration file tells 11ty what to do. Setting the directory to tells 11ty where to look for changes, this is our working directory. When changes are detected, 11ty builds the site and outputs it to the directory which is where the static html/css/img files are served from, amazing! As we’re going to be keeping our homepage code in version control, create a file in the project root: Open the file in VSCodium and add the following and save: The .gitignore file is a text file that tells Git which files or folders to ignore in a project. In this case, our file tells git to ignore the directory and the directory where our static files are built locally. Now comes the fun part, building our homepage. 11ty supports a number of templating languages, but the two you’ll reach for most are Markdown and plain HTML. Markdown is the popular choice for content like blog posts: you just write, without tags getting in the way. HTML is handy when you need precise structure. The best part is you can drop HTML straight into a Markdown file and 11ty renders it correctly, so it’s never one or the other. For the pages that make up the site’s structure (home, about, links, contact) we’ll use HTML, because it maps neatly onto the layouts and partials we’re about to build. When we get to the blog, we’ll write the posts in Markdown, where it shines. Use whichever fits the job. Create a directory at the project root and cd into it: Create an file in the terminal or VSCodium: Open the file and add some content: Now from the terminal start 11ty: If everything has been configured right so far you should see the following: Now you can open up and check out your new 11ty homepage! It should look like this: A Basic Hello World HTML Page Amazing! But what we want to avoid is having to write out the and and tags on each and every page, and be able to include a site header, navigation and footer so we don’t have to copy and paste the changes across every page each time we update. Let’s checkout templating a layout! Create a new directory in the directory and cd into it: Create a file in the terminal or VSCodium: Open the file and add the following: We've created as a Nunjucks template file, hence the file extension. This means we can use Nunjucks' double curly braces for using frontmatter variables. In our layout template we're calling and . Now, head back to the file you created earlier, delete the contents and add some front matter and some content: If you’ve kept 11ty running and the browser running it should look like this: A Basic Hello World HTML Page Using a Template Amazing! Now lets create the additional pages for our homepage. Create the following pages in the directory with the terminal or VSCodium: Open each of them up and add in some front matter and content: about.html: links.html: contact.html: You should now be able to browse each of these pages if you kept 11ty running on the following urls: Great stuff, but that’s no use without a navigation! Let’s take a look at and create a shared , , and to bring our homepage together. In the terminal cd into and create three partial files: Open each of them up and add some content: header.njk: navigation.njk footer.njk: Once our partials are created, open again and update it to include our new elements and partials: If you’ve kept 11ty running and the browser running it should look like this: A Basic Hello World HTML Page Using a Template and Partials Amazing! Now lets add the blog. Blog posts are mostly prose, so this is where Markdown earns its keep. We’ll write the posts as files and let 11ty turn them into pages. Create a new directory in the directory and cd into it: Create the following files in the directory with the terminal or VSCodium: Awesome, Open each of them up in VSCodium and add the following: my-first-post.md : my-second-post.md : my-third-post.md We better create a blog layout so it renders! Head back to the directory to create a new layout file: Open up in VSCodium and add the following: Check that your blog posts are loading: Amazing right? But to make it a blog, we need a blog page that lists all of our blog posts. We can do this with a collection: Open again and add a key called with a value of : Now 11ty has created a collection called and all we have to do is list it. Head back to the directory and create a file: Open it and add the following: If you’ve kept 11ty running and the browser running it should look like this: A Basic Blog List Page Amazing huh? Great, so far we have a fully functional home page, but it doesn’t look quite right. We need a style sheet. You can use the one below as an example, it’s basic styling with some modern techniques, or just throw in your own! Create a new directory in , cd into it and create : Open in VSCodium and add the following: styles.css: Now we need to include the style sheet in our layout file. Open it up and add to the : _includes/base.njk: You would have noticed that the stylesheet hasn’t been applied, we have to do one more thing in , something called file passthrough copy. Open in VSCodium and add the following: Because this will come up we may as well create the directories and add in the configuration for our images, fonts and JavaScript files. Create the following directories in : Update again: Just make sure you put all your static files in the appropriate directory and you’ll be good. So finally, if you’ve kept 11ty running and the browser running it should look like this: A Nicely Styled Homepage Yours will look a little different depending on the colours and fonts you chose above. Now we have a homepage we’re happy with, let’s get it online. There are two ways to get your site onto Neocities. We’ll start with the simplest, pushing it from your terminal by hand, then automate it so a deploy happens every time you commit. Whichever method you choose, first build a fresh copy of your site: This writes the finished HTML, CSS and assets to the directory. That’s the folder we deploy. Neocities provides a command-line tool that lets you push your site straight from your terminal. It’s a Ruby gem, so you’ll need Ruby installed. The first time you run a command it’ll ask for your Neocities username and password, then store an API key locally so you don’t have to log in again. Push the contents of your directory: That’s it, your homepage is live. For a lot of people this is all you need. Build, push, done. Pushing by hand is fine, but it’s even nicer to have your site rebuild and deploy itself every time you commit a change. We can do that with Forgejo Actions , the built-in CI for Forgejo. If you self-host Forgejo this runs on your own runner; if you don’t self-host, Codeberg offers the same thing (more on that below). First, push your project to a repository on your Forgejo instance. Then grab your Neocities API key from your account settings (Manage Site Settings → API Key) and add it to your repository as a secret named (Repository → Settings → Actions → Secrets). Now create a workflow file at : A few things to note in this workflow: Commit and push the workflow file. From now on, every push to rebuilds your site and deploys it to Neocities automatically. If you don’t run your own Forgejo instance, Codeberg is a free, community-run home for your code and runs the very same Forgejo Actions. The workflow file above works as-is. Push your project to a Codeberg repo, add the secret in the repository settings, and you’re away. You may need to enable Actions for your repository first; see the Codeberg CI documentation for details. Already have a homepage you’ve been hand-coding on Neocities? You don’t have to start from scratch. Eleventy is happy to take what you’ve got and slot it into this structure. Copy each existing page into (your old becomes , and so on). Then move the parts every page repeats, the , header, nav and footer, into and the partials you built earlier. Delete that boilerplate from each page and add a little front matter at the top: Whatever’s left in the file is just that page’s own content, and the layout wraps it. Your CSS goes in , images in , and fonts in . The passthrough copy we set up earlier ships them straight to . If a page is mostly writing, paste the body into a file instead of . Any fiddly HTML, like an embed or some custom markup, can stay exactly as it is and 11ty will render the Markdown around it. Run , check looks the way you expect, then push it live with the Neocities CLI or your Forgejo Actions workflow. Same site you already had, now with layouts, partials and a build step doing the repetitive work for you. Reference: I created the original version of this guide based heavily on these existing guides, and they’re still well worth a read: Without these, I wouldn’t even know how to write down what I needed to. Hey, thanks for reading this post in your feed reader! Want to chat? Reply by email or add me on XMPP , or send a webmention . Check out the posts archive on the website. You have a basic understanding of HTML and CSS You have a basic understanding of the command line and terminal You have Node.js installed (version 18 or newer) You're using VSCodium as your editor You have a Neocities account You have somewhere to keep your code: a Forgejo instance or a Codeberg account http://localhost:8080/blog/my-first-post/ http://localhost:8080/blog/my-second-post/ http://localhost:8080/blog/my-third-post/ picks the runner label. This is the default on Forgejo and Codeberg. Actions are referenced by their full URL. The checkout and setup-node actions come from , so we stay off GitHub for those. The deploy step uses , which is hosted on GitHub. We're only using it. Your code still lives on Forgejo or Codeberg. The option removes remote files that aren't in your new build, the same as on the CLI. Create Your First Basic 11ty Website Itsiest, Bitsiest Eleventy Tutorial

0 views

Plugins case study: Pluggy

Recently I came upon Pluggy , a Python library for developing plugin systems. It was originally developed as part of the pytest project - known for its rich plugin ecosystem - and later extracted into a standalone library. You're supposed to reach out for Pluggy if you want to add a plugin system to your tool or library and want to use something proven rather than rolling your own. In this post I will share some notes on how Pluggy works, and will then review how it aligns with the fundamental concepts of plugin infrastructures . Pluggy is built around the concept of hooks : functions that host applications or tools (from here on, just "hosts") expose and plugins implement. A host exposes hooks by using a decorator returned from pluggy.HookspecMarker and a plugin implements this hook using a decorator returned from pluggy.HookimplMarker . Pluggy's documentation explains this fairly well; in this post, I'll show how to implement the htmlize tool with some plugins, introduced in the original article in my plugin series . As a reminder, htmlize is a toy tool that takes markup notation similar to reStructuredText, and converts it to to HTML. It supports plugins to handle custom "roles" like: As well as plugins that do arbitrary processing on the entire text. Out host defines two hooks: A hook is created by calling HookspecMarker with the project's name. This project name has to match between the host and its plugins. Pluggy is permissive about what hooks accept as parameters and what they return; for maximal flexibility and to stay true to the original htmlize example, our hooks return functions. To accompany this HookspecMarker , the host also defines a HookimplMarker with the same name: This is used by plugins to attach to hooks when they're loaded. The host's main function loads plugins at startup as follows: hookspecs is our Python module containing the hooks shown above. load_setuptools_entrypoints is Pluggy's helper for loading plugins that were pip -installed into the same environment and registered as setuptools entry points . It's a way to signal - in one's setup.py or pyproject.toml file - some metadata that projects can review at runtime. In our project, the plugins register themselves with this section in the pyproject.toml file: This says "for entry point htmlize , define a new entry named tt ". Pluggy's load_setuptools_entrypoints then uses importlib.metadata to access this information. Note that Pluggy doesn't require using this mechanism. Hosts can implement any plugin discovery method they want, and add plugins directly to their PluginManager with the register method. But this is the mechanism used for pytest and many other projects; it makes it very easy to automatically discover and register plugins that are installed with pip and equivalent tools. Once PluginManager loads the plugins, invoking them is straightforward; here's how htmlize invokes the contents hooks [1] : Generally, hook invocations return a list of all the hooks attached to by different plugins (a single host application can have multiple plugins installed and attaching to the same hook). When the host invokes the hook as shown above, the default order is LIFO, but plugins can affect this with hook options like tryfirst and trylast . Here's our entire narcissist plugin that's attaching to the contents hook: Some notes: Let's see how this case study of Pluggy measures against the Fundamental plugin concepts that were covered several times on this blog . It's important to remember that Pluggy is not a specific host application with a bespoke plugin system; rather, it's a reusable library for creating such plugin systems. Therefore, this is more of a meta case study. Generally, Pluggy leaves discovery logic to the user's discretion. Its PluginManager has a register method for adding plugins, and these can be discovered in any way the application chooses. That said, Pluggy comes with one discovery mechanism built in - through the entry points process of Python packaging, as shown above. This is hugely convenient for a large number of applications, as long as both the application and its plugins are installed via standard Python packaging tools (which is a very reasonable assumption in the Python ecosystem). In the entry point process, plugins register themselves by adding a [project.entry-points.<HOST-ID>] section in their pyproject.toml file. Otherwise - as in the previous section - users are free to devise their own registration schemes. This one is easy, since it's called hooks in Pluggy parlance as well! Pluggy's implementation of hooks is rather elegant, with function decorators available for plugins to set. We've seen an example of this above with @htmlize.hookimpl decorating htmlize_contents . Since Pluggy is designed for Python hosts and Python plugins, this one is fairly straightforward. The plugins typically assume the host project is already installed in the Python environment and its modules can be imported. In our example, hookimpl is imported from htmlize by the plugin to accomplish this. It also shows how host data is passed to the plugin - the post and db parameters. These are APIs exposed by the host for the plugins' use. In footnote 2 of my original fundamental concepts of plugin infrastructures post, I wrote [2] : I still believe my statement is true - plugin frameworks are very easy to create, and the functionality they provide is relatively small compared to their large surface area. In other words, this is a shallow API . That said, Pluggy does provide some nice functionality for the more advanced uses of plugins: Are these worthwhile for your project? It really depends on the project, and it's always worth keeping the tradeoff between dependencies and project effort in mind. The full code repository for this post is available here . It expects htmlize to be installed; as discussed previously, we rely on Pluggy's default install-based approach where both the host and plugins are installed into the same Python environment and can thus find each other. However, Pluggy supports any custom discovery method. It uses the hookimpl exported value shown earlier. It returns a function that acts on contents; this is the htmlize -specific contract (ABI, if you will) we've discussed before. Automatic entry point registration mechanism - if you need it Signature validation Consistent plugin result collection across multiple hook attachments in a single plugin and across many plugins Plugin ordering with firstresult , tryfirst , trylast , etc. Hook "wrappers" for some special use cases

0 views
Simon Willison 1 months ago

Publishing WASM wheels to PyPI for use with Pyodide

The Pyodide 314.0 release announcement (via Hacker News ) includes news I've been looking forward to for a long time: You can now publish Python packages built for Pyodide (or any Python runtime compatible with the PyEmscripten platform defined in PEP 783 ) directly to PyPI and install them at runtime. Previously, the Pyodide maintainers had to maintain, build, and host over 300 packages ourselves. This created a significant burden on our maintainers and became a major bottleneck for the community, as every new package required manual review. Moving forward, package maintainers can simply build and publish Pyodide wheels to PyPI, just as they do for native wheels on Linux, macOS, or Windows. Here's the PR to PyPI itself supporting this , which landed on April 21st. I adore Pyodide , and have been frustrated in the past by this limitation. It's possible to compile C or Rust extensions to WASM in a wheel file, but before now there was no easy way to distribute them. Thanks to the efforts of a whole lot of people, that's now been fixed! I decided to celebrate by finding something I could package. I have quite a few experimental Pyodide projects lying around, but the best fit for this looked to be my Luau WebAssembly research spike from 9th March. Luau is a "small, fast, and embeddable programming language based on Lua with a gradual type system", developed by Roblox and released under an MIT license. It's written in C++. I already knew it was possible to compile it to WebAssembly and get it running inside of Pyodide, so I set Codex + GPT-5.5 xhigh the task of packaging my experiment up and publishing it to PyPI using GitHub Actions. It took some iteration, but here's the result: luau-wasm is a brand new PyPI package which publishes a 276KB file which can be used in Pyodide like this: You can run that code in the Pyodide REPL demo to see it in action. The GitHub repo for luau-wasm includes all of the build and deploy scripts (using the latest cibuildwheel ) and also deploys an HTML demo page which loads Pyodide, installs and provides an interface for trying it out: https://simonw.github.io/luau-wasm/ I was curious to see how many packages are currently publishing wheels for this platform. After some tinkering with ChatGPT I got to this BigQuery SQL which I ran against PyPI's public dataset on BigQuery . Here's the raw JSON of query results and here's a SQLite SQL query in Datasette Lite which dedupes packages by most recent upload date. If the query is right, there are currently 28 PyPI packages publishing with the new tags: luau-wasm , uuid7-rs , cmm-16bit , pyOpenTTDAdmin , imgui-bundle , numbertoolkit , bashkit , geoarrow-rust-core , arro3-io , arro3-core , arro3-compute , onnx , powerfit-em , tcod , chonkie-core , tokie , robotraconteur , pydantic_core , yaml-rs , cadquery-ocp-novtk-OCP.wasm , uuid_utils , base64_utils , pycdfpp , lib3mf-OCP.wasm , typst , toml-rs , onnx-weekly , dummy-pyodide-ext-test Here's hoping we see a whole lot more of those showing up over the coming months and years. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options .

0 views
Simon Willison 1 months ago

Claude Fable is relentlessly proactive

After two days of experience with Claude Fable 5 I think the best way to describe it is relentlessly proactive . It knows a whole lot of tricks and it will deploy pretty much any of them to get to its goal. I'll illustrate this with an example. I was hacking on Datasette Agent today when I noticed a glitch: a horizontal scrollbar that shouldn't be there in the jump menu chat prompt. I snapped this screenshot: Then I started a fresh session in my checkout, dragged in the screenshot and told it: I had a hunch the cause was in a dependency of Datasette Agent (likely Datasette itself) and I knew Fable was good at digging into dependency code, either by inspecting installed files in its own virtual environment or by referencing a local checkout on disk. Telling it to start with dependencies felt like a good bet. I got distracted by a domestic task and wandered away from my computer. When I came back a few minutes later I saw my machine open a browser window in my regular Firefox and then navigate to the dialog in question . I had not told Claude Code to use any browser automation, and I was pretty sure it wasn't possible for it to trigger mouse movements or keyboard shortcuts within a window, so how was it doing that? I watched in fascination as it continued with its explorations, then saw it open a Safari window instead of Firefox. I also grabbed this snapshot from the Claude terminal: What was it doing there with ? It turns out Fable had hacked up its own pattern for taking screenshots of browser windows. It was using Python to iterate through all available windows on my machine, then filtering for Safari windows with expected strings such as in the window name. It used that to find their window number - an integer like 153551 - which it could then use with the CLI tool to grab a PNG. OK fine, that's a neat way of taking screenshots. But what was it taking screenshots of? Turns out it had been writing its own scratch HTML pages to try and recreate the bug, then opening Safari and grabbing screenshots. Here's that /tmp/textarea-scrollbar-test.html page it created, and the screenshot it took with : (I have way too many open tabs!) OK, so I can see how it's opening test pages and taking screenshots, but how on earth was it triggering the modal dialog that was meant to be under test? That's only available via a click or a keyboard shortcut, and I couldn't see a mechanism for it to run those in Safari. I eventually figured out what it had done. Claude was running in a folder that contained the source code for the application. It knows enough about Datasette to be able to run a local development server. It turns out it was editing Datasette's own templates to add JavaScript that would trigger the correct keyboard shortcut as soon as the window opened, adding code like this: 1.2 seconds after the window opens, this code triggers a simulated key, which is the keyboard shortcut for opening the modal dialog. There was one challenge left. In order to understand what was going on, Claude needed to run JavaScript on the page to take measurements for itself. It wrote its own custom web application to capture information via CORS, then ran that as a local server and opened a page with JavaScript that would POST directly to it! Here's the Python web app it wrote, using the standard library http.server package: All this does is accept a POST request full of JSON and write that to the file. It sends headers (including from requests) so that code running on another domain can still communicate back to it. Then Claude injected this code into the template that it was loading in a browser: This took measurements of the inside the Web Component and sent them to the server, which wrote them to a file on disk, which Claude could then read. Having figured out all of these tricks Fable... hit some invisible guardrail and downgraded itself to Opus. Thankfully Opus had access to the full transcript and could continue using the tricks pioneered by Fable, and shortly afterwards found, tested and verified the fix . I prompted Opus to: Which produced this report , which was invaluable for piecing together the details of what had happened for this post. I've shared the full terminal transcript of the Claude Code session as well. Based on a screenshot and a one-line prompt, Claude Fable 5 + Claude Code: Like I said, relentlessly proactive! I'm currently on the $100/month Claude Max plan, which includes a generous allowance for Fable up until June 22nd after which Anthropic say they'll start charging full API prices for it. I'm using AgentsView to track my spending (see this TIL ). Here's what AgentsView says this session would have cost me if I was paying full price for it: If you don't keep a close eye on it, Fable will quite happily burn $12 in tokens inventing new ways to debug your CSS. On the one hand, watching Fable go to extreme lengths to get the information that it needed to debug what was, in the end, a two-line CSS fix, was fascinating . But on the other hand... this is a robust reminder that coding agents can do anything you can do by typing commands into a terminal - and frontier models know every trick in the book, and evidently a few that nobody has ever written down before. If Fable had been acting on malicious instructions - a prompt injection attack hidden in code or an issue thread, or something I'd carelessly pasted into my terminal - it's alarming to think quite how far it could go to exfiltrate data or cause other forms of mischief. Running coding agents outside of a sandbox has always been a bad idea - it's my top contender for a Challenger disaster incident, as described by Johann Rehberger in The Normalization of Deviance in AI . Fable is arguably smarter and hence more suspicious of potentially malicious instructions. But that smartness is very much a two-edged sword: if it does get subverted by instructions, the amount of damage it can do given its relentless proactivity is terrifying. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . Figured out the recipe to run the local development server (with fake environment variables needed to get it running) Fired up a Playwright Chrome session Turned on the visible scrollbars setting for Chrome (it turned that off again later) Cycled through Firefox and WebKit in Playwright too, failing to recreate the bug Worked out my default browser was Safari Built a HTML document Opened that in real (not Playwright) Firefox Found that was blocked because "osascript is not allowed assistive access" Figured out that workaround, described above Added JavaScript to the site templates in order to trigger the key Built its own little Python CORS web server to capture JSON data Rewrote the template to capture that data and send it to the server Scripted its way through the Web Component shadow DOM to the information it needed Opened Safari to confirm the source of the bug Modified its custom template to hack in a potential fix Confirmed the hacked fix worked Reported back on how to fix the problem

0 views