Posts in Javascript (20 found)

Running Python code in a sandbox with MicroPython and WASM

I've been experimenting with different approaches to running code in a sandbox for several years now, but my latest attempt feels like it might finally have all of the characteristics I've been looking for. I've released it as an alpha package called micropython-wasm , and I'm using it for a code execution sandbox plugin for Datasette Agent called datasette-agent-micropython . My key open source projects - Datasette , LLM , even sqlite-utils - all support plugins. I absolutely love plugins as a mechanism for extending software. A carefully designed plugin system reduces the risk involved in trying new things to almost nothing - even the wildest ideas won't leave a lasting influence on the core application itself. My software can grow a new feature overnight and I don't even have to review a pull request! There's one major drawback: my plugin systems all use Python and Pluggy , and plugin code executes with full privileges within my applications. A buggy or malicious plugin could break everything or leak private data. I'd love to be able to run plugin-style code in an environment where it is unable to read unapproved files, connect to a network, or generally operate in a way that's risky or harmful to the rest of the application or the user's computer. My interest covers more than just plugins. For Datasette in particular there are many features I'd like to support where arbitrary code execution would be useful. I've already experimented with this for Datasette Enrichments , where code can be used to transform values stored in a table. I'd love to build a mechanism where you can run code on a schedule that fetches JSON from an approved location, runs a tiny bit of code to reformat it into a list of dictionaries, then inserts those as rows in a SQLite database table. My goal is to execute code safely within my own Python applications. Here's what I need: Web browsers operate in the most hostile environment imaginable when it comes to malicious code. Their job is to download and execute untrusted code from the web on almost every page load. Given this, JavaScript engines should be excellent candidates for sandboxes. Sadly those engines are also extremely complicated, and are not designed for easy embedding in other projects. Most of the v8-in-Python projects I've seen are infrequently maintained and come with warnings not to use them with completely untrusted code. WebAssembly is a much better candidate. It was designed from the start to support all of the characteristics I care about and has been tested in browsers for nearly a decade. The wasmtime Python library is actively maintained and has binary wheels. WebAssembly engines like wasmtime run WebAssembly binaries. Some programming languages like Rust are easy to compile directly to WebAssembly. Dynamic languages like JavaScript and Python are harder - they support language primitives like , which means they need a full interpreter available at runtime. To run Python we need a full Python interpreter compiled to WebAssembly, wired up in a way that makes it easy to feed it code, hook up host functions and access the results. Pyodide offers an outstanding package for running Python using WebAssembly in the browser, but using Pyodide in server-side Python isn't supported. The most recent advice I could find was from October 2024 stating "Pyodide is built by the Emscripten toolchain and can only run in a browser or Node.js". The other day I decided to take a look at MicroPython as an option for this. The MicroPython site says: MicroPython is a lean and efficient implementation of the Python 3 programming language that includes a small subset of the Python standard library and is optimised to run on microcontrollers and in constrained environments. WebAssembly sure feels like a constrained environment to me! I had GPT-5.5 Pro do some research for me , which turned up this PR against MicroPython by Yamamoto Takahashi titled "Experimental WASI support for ports/unix". It then produced this research.md document , so I let Codex Desktop and GPT-5.5 high loose on it to see what would happen: It worked. I now had a prototype Python library that could execute Python code inside a WebAssembly sandbox! The trickiest piece to solve was persistent interpreter state. The WASM build we are using here exposes a single entry point which starts the interpreter, runs the code and then stops the interpreter at the end. This works fine for one-off scripts, but for Datasette Agent I want variables and functions to stay resident in memory so I can reuse them across multiple code execution calls. A neat thing about working with coding agents is that you can get from an idea to a proof of concept quickly. I prompted: After some iteration we got to a version of this that works! In Python code you can now do this: Under the hood this starts a thread, sets up a request queue and then sends messages to that queue for the command, each time waiting on a reply queue for the result of that execution. Inside WASM the MicroPython interpreter blocks waiting for a host function to return the next line of code, which it runs on before calling when each block has been successfully executed. The other piece of complexity was supporting host functions, so my Python library could selectively expose functions that could then be called by code running in MicroPython. Codex ended up solving this with 78 lines of C , which ends up compiled into the 362KB WebAssembly blob I'm distributing with the package. I am by no means a C programmer, but I've read the C and had two different models explain it to me (here's Claude's explanation ) and I've subjected it to a barrage of tests. The great thing about working with WebAssembly is that if the C turns out to be fatally flawed the worst that can happen is the WebAssembly execution will fail with an exception. I can live with that risk. Memory limits are directly supported by wasmtime. CPU limits are a little harder: wasmtime offers a "fuel" concept to limit how many operations a WebAssembly call can execute, and that's the correct fit for this problem, but the units are hard to reason about. I'm experimenting with a 20 million default "fuel" setting now but I'm not confident that it's the most appropriate value. The alpha is now live on PyPI . You can try it from your own Python code as described in the README . I've also added a simple CLI mode in version 0.1a2 which means you can try it using without first installing it like so: You can also try it in Datasette Agent like this: Then navigate to http://127.0.0.1:8001/-/agent and run the prompt: Having complained about immature, loosely-maintained sandboxing libraries, it's deeply ironic that I've now built my own! I deliberately slapped an alpha release version on it, and I'm not ready to recommend it to anyone who isn't willing to take a significant risk. I've put it through enough testing that I'm OK using it myself. I've shipped my first plugin that uses it, datasette-agent-micropython . I've also locked GPT-5.5 xhigh in that Datasette Agent plugin and challenged it to break out of the sandbox and so far it has not managed to. I'm hoping this implementation can convince some companies with professional security teams and high-stakes problems to commit to using Python in WebAssembly as a sandboxing approach and open source their own solutions. 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 . Why do I want a sandbox? What I want from a sandbox WebAssembly looks really promising here MicroPython in WebAssembly Building the first version Try it yourself Should you trust my vibe-coded sandbox? Dependencies that cleanly install from PyPI , including binary wheels across multiple platforms if necessary. I don't want people using my software to have to take any extra steps beyond directly installing my Python package. Executed code must be subject to both memory and CPU limits. I don't want to crash my application or the user's computer. File access must be strictly controlled . Either no filesystem access at all or I get to define exactly which files can be read and which files can be written to. Network access is controlled as well . Sandboxed code should not be able to communicate with anything without going through a layer I fully control. Support for interaction with host functions . A sandbox isn't much use if I can't carefully expose selected platform features to the code that it's running. It has to be robust, supported, and clearly documented . I've lost count of the number of sandbox projects I've seen in repos with warnings that they aren't actively maintained!

0 views
iDiallo Today

Why all the PRs?

It's a signal. That's why we get AI-generated PRs. We told everyone, in order to get your resume taken seriously, you need to show your work. When I was getting started in my career, that meant having your own website that you contribute to regularly. So I did that. I built websites, I maintained them. I kept maintaining them even after I got the jobs because that's how I actually honed my web programming skills. Where else was I going to try new frameworks, a new JavaScript paradigm, or try out Ruby on rails? I got the job, and I advised other developers to follow the same path. But then github became mainstream. Rather than just show a finished website, you could actually share the code that runs your project. Share a link to your github project and companies can review your code and directly gauge your experience. But even better, you can show your contribution to open source projects. Not just any projects. Popular projects. The github stars became a metric people look for. A signal that can be used to quickly assign a value to a candidate. But that’s the story told from the outside. I don’t think the github profile link was ever important, unless it was significantly good. Employees focused on their work rarely have the time to maintain healthy github activity. Their experience comes from their day to day job. So for the most part, not much attention was placed on github links other than skimming through those surface level details. When stacks of resumes came on my desk, the best candidates stood out because they had work experience. The good candidates had projects that they could link to, github or elsewhere. But then, the worst candidates had long padded resumes that had elements of every job application tips-and-tricks-article. They had a website, but it was built in a day for the purpose of getting a job, with nothing interesting to say. They had github links, but those often pointed to school projects, homework, or boilerplate code. That’s the vast majority of github links I used to get. People with active and well maintained github profiles were rare. Rare because it actually requires time, effort, and experience. But then we have AI. There was a golang auth issue that I've contributed to on github. It was already a few years old when I proposed a solution that worked for my case. It wasn't universal so it wasn't accepted. The discussion is revived every couple years, each person bringing one more piece to the puzzle. But then recently, someone exploded the thread with comments. And even created a PR to go with it. This was from a user that went from a dormant account to 4000 contributions in a year. It was all AI assisted code. This isn’t to comment on the quality of his code, but he was clearly trying to optimize the metric. Looking at his linkedin profile, he doesn’t work in a software engineering role, and it’s hard to decide if he would be a good contributor if hired. If we were to judge his resume by looking at the github profile, it might catch our attention. But then, there is a problem. There are hundreds, even thousands of people all doing the same thing. They are cranking up their contributions to github projects using AI, so they can have a better chance at getting hired as developers. I understand the job market is rough right now, especially for gen z , and anything to differentiate yourself is a plus. The problem is this is being done at the expense of open source projects. The contributors are not submitting PRs to your project because they are personally invested in it. Instead, they are trying to get their name on the contributors list so that they can use it as a signal in their resume. When we are out here debating if there is any merit in AI generated PRs, or if we should just judge the code, we tend to miss that their gesture is completely hollow. The PR’s author intentions are completely misaligned with the project's maintainers. They are playing a different game. We call it slop, or a waste of time, we ban them and they get really vocal about expressing their first amendment rights. We are directly interfering with their goal of padding their resume. I often ask, why don’t people who create those PRs not just start their own project? One answer I’m starting to believe is, nobody cares about a github profile with a handful of stars. You need to contribute to a popular project. Most if not all AI generated websites look the same, it doesn’t matter how well you customize the prompt. Most greenfield projects from new programmers look the same, the prompter lacks the experience to do anything different. Contributing to open source is a scary thing when you are new. Even when you have experience, it’s a deliberate act. You have to be invested in the work. Just like asking questions on stackoverflow, issues you raised will often get closed . And when they do, you have to learn from it. The value of an open source contributor is not in the volume of work they can perform. If you skim any important projects, you’ll see that the best contributors spend more time discussing the problem than writing code. Their value is in solving problems and contributing to the collective memory of the group. But when you are doing a drive-by PR that may or may not be correct, and you are just trying to get your name on a list, you are providing zero value to the maintainer. Just more work. This is the signal every slop PR generator is after.

0 views

Attribution in the Browser: Who Really Benefits from Google and Meta’s New Privacy Standard

Google, Meta and the unlikely addition of Mozilla are teaming up to work on a browser W3 specification that would add browser user agent features to track impression, click data and ‘conversion’ data. This data is then sent to respective parties Ad Impression → Ad Networks Conversions → Advertiser So far this is just a duplication of what is naturally tracked by each party, ie just their own resources. The difference then is that this data is forwarded by each party to an attribution service provider (Google/Meta/Mozilla) who aggregates and returns conversion histograms to the Ad network: Ad Impression → Browser Function → Ad Network → Attribution Provider Conversion → Browser Function → Advertiser → Attribution Provider At first glance, this would nearly seem like any expected flow of ad data, but here the “Advertiser” seems to stand in for the Ad Network , working on behalf of the advertiser. Why? Because otherwise the owner of the site would need to manage the attribution selection and call along with then updating the Ad Network. So what realistically will happen? Ad networks will require JS pixels to be dropped on the advertisers site to manage the and attribution process. So the real process: Ad Impression → Browser → Ad Network → Attribution Provider Conversion → Browser → Ad Network (via pixel on advertiser site) → Attribution Provider Firstly, this mostly seems to be a fix for situations where Cookies are removed. How is this problematic for users? As a user I do not mind when an advertiser (eg Nike) tracks what blog I came from to Nike website. My concern is a Meta / Google that tracks every site I was on and went to. So in this way, I think advertisers and users should be aligned. Mobile attribution is based on device fingerprinting. While MMP companies like AppsFlyer (45% app market share) are not mentioned, there could be some potential for MMPs to work on behalf of an mobile advertiser to call and gather attribution from mobile web to app that is *not* just fingerprinting. AppsFlyer has recently released web2app which, despite the hype, has the usual probabilistic and short lookback windows for deferred deep link installs. MMPs would have a strong desire to move from probabilistic to something more deterministic. The problem? The usual, what’s always kept mobile and digital ad measurement separate. A WebView opened by the advertiser app does not have access to the device regular browser cookies. Given that mobile operating systems are run by the ad networks Apple and Google respectively, you could see this some change here if the browser Attribution API comes to pass. Still they would likely have a hard time carving out a space for MMPs to stay between the ad network and advertiser. The API has the potential to support small web publishers but the danger that this is simply co-opted by the ad monopolies to consolidate their positions is real. With the idea of browser tracking ads, why not move the whole process into the local browser, completely cutting out the server calls until a conversion is recorded? This is a popular idea but would add potential avenues for ad fraud where a can be called locally where the ad network ecosystem wide resources to realize this is likely a fraudulent impression would be lacking. I think there is room there for other fraud fighting models to help, but the obvious threat of this type of fraud will likely keep a completely local attribution model from being developed for now. This leads to the current W3 spec and where we are now. I see real positives and negatives to the suggested specs. I’ll be keeping an eye on it in the coming weeks to months (deadline is November 2026 for the working group to finish) and see how it develops, or if this gets added to the Privacy Sandbox graveyard.

0 views

How Other Link Checkers Do Recursion

After I published Five Years of Trying to Add Recursion to lychee , one reply I got was a very fair question: If recursion is so hard, how do other link checkers do it? Plenty of them already crawl websites! This sent me down a rabbit hole of reading the code of other link checkers. The key takeaway is: they didn’t find a clever trick we missed. They were built as crawlers from the very first commit, and I initially built lychee as a stream. I went and read the source of the recursive checkers we list in lychee’s README : muffet (Go), LinkChecker (Python), linkinator (TypeScript), and broken-link-checker (JavaScript). This post is a teardown of how each one actually handles recursion, what it costs them, and what it means for lychee. If you haven’t read the first post , the summary is that lychee was architected as a one-shot, unidirectional pipeline ( ). Recursion needs a cycle (responses create new inputs), and cycles in an async, channel-based pipeline are where the dragons live . 🐲 Five years and four attempts later, the pieces we’ll need to do it properly only just landed. DAGs vs. cycles Every recursive checker I looked at is built from the same three parts: Diagrammatically, lychee is different from the others: Crawlers have a back-edge baked in. Our pipeline doesn’t, and every one of my failed attempts was an effort to bend that back-edge into a graph that was never designed for it. Let’s look at that graph design more closely: Note that the visited check happens in the enqueue step, atomically with the mark, before the worker ever touches the network. That ordering is the entire fix to the deduplication race that haunted lychee’s attempts 1–4, where the cache was written after checking. Each tool uses a variation on it. muffet (Go): a WaitGroup and a Set muffet is closest in spirit to lychee: a fast, single-binary, concurrent website checker. The dedup + scheduling decision lives in one method ( ): is a (a mutex-guarded ). returns whether the URL was already present, so a page is only scheduled the first time it’s seen. Dedup happens at enqueue, synchronized by the set’s mutex. This is basically a line-by-line translation of the diagram above. Checking a page fetches all of its links concurrently, and feeds qualifying ones back into , the back-edge: How muffet knows it’s done muffet’s answer to termination is a little built around a ( ): Every scheduled page increments the group; every completed page decrements it; returns when the count hits zero. The whole crawl bootstraps with a single before , so the counter is positive before anyone waits on it. This is the same counter I tried (and failed with) in Attempt 1 and Attempt 4 . The difference is the invariant: is only ever called from inside an already-running daemon that holds the count above zero (or from the bootstrap). There is no window where the counter briefly reads zero while work is still pending. Go’s enforces this invariant so naturally that it doesn’t feel like distributed termination detection at all, but that’s exactly what it is. It’s the moral equivalent of the primitive Kait contributed to lychee in 2026 . Where the tradeoffs are Concurrency isn’t bounded by the daemon manager. does for every task, spawning unbounded goroutines. The actual limiting happens downstream in a (a buffered-channel counting semaphore) and a per-host throttler pool. muffet separates “the frontier” from “the rate limiter,” which is exactly the separation lychee lacked when it tried to use one bounded channel as both in the past. Cheap goroutines do a lot of heavy lifting. Spawning a goroutine per link is “fine” in Go. The equivalent in Rust ( per link, each needing state) is what pushed me toward and the ownership pain I wrote about . On extensibility, muffet is a focused CLI, not a library. There’s no plugin surface; you get what the flags give you. lychee deliberately ships as a reusable crate, which raises the bar, since every architectural choice has to uphold the standards of a public API. On scalability, unbounded goroutines plus an in-memory visited set scale comfortably to large sites, but there’s no disk-backed frontier, so a truly enormous crawl is bounded by RAM. Same as lychee. Takeaways: muffet LinkChecker (Python): a joinable unbounded queue LinkChecker has existed since the year 2000. It’s a synchronous, thread-pool crawler. Its frontier is a hand-written ( ), a clone of Python’s with / . Look at the very first design comment: It’s explicit about the exact deadlock that bit me. That comment is our Attempt 4 backpressure deadlock , called out and designed around. lychee tried to push discovered URLs into a bounded channel; when it filled, the response handler blocked, no responses drained, no slots freed. Deadlock. 💥 LinkChecker’s answer is brutalist in nature: the frontier is unbounded . Backpressure is enforced elsewhere (a fixed thread count and per-host throttling), never by blocking a producer that is also a consumer. Termination by counter, done right blocks until hits zero ( ): Again: a counter. But the increment in and the decrement in are both inside the queue’s lock, and a worker calls only after fully processing an item including enqueuing its children . So children are counted before the parent is marked done, with no premature zero. It’s semantics implemented with a mutex and a condition variable. Deduplication, before the request LinkChecker writes the URL into its result cache at enqueue time ( ): That sentinel is a “fix” that’s missing in lychee’s attempts. By the time any worker thread checks the URL, the cache already says “mine,” so concurrent discovery from another page is a no-op. Per-host politeness and termination guards The ( ) throttles per host: and calls so a stuck crawl can’t hang forever. Where the tradeoffs are Blocking threads instead of async. Each of the (default 10–100) threads does blocking I/O via . Simple and battle-tested, but the concurrency ceiling is the thread count, and each thread carries a full stack. lychee’s Tokio model reaches thousands of concurrent in-flight requests on a handful of OS threads; LinkChecker can’t, and doesn’t try. The unbounded frontier trades a deadlock for unbounded memory. The explicit “no max size” decision means RAM growth on huge sites. There’s a cap and a periodic to mitigate it. Extensibility is excellent. LinkChecker has a real plugin system ( : anchor checks, SSL, virus scanning, and more) and many output loggers. This is the most extensible of the bunch, and it pays for that with a large, mature, somewhat old-fashioned codebase. On scalability, it’s GIL-bound and thread-limited, so raw throughput is the lowest here, but correctness and feature coverage are high. Takeaways: LinkChecker linkinator (TypeScript): Single-Threaded linkinator is a Node.js checker, and it benefits from something neither Go nor Rust provides: a single-threaded event loop . Check-and-insert into the visited set is atomic for free , because no two callbacks run simultaneously. The frontier is a concurrency-limited (a p-queue-style structure). Termination is one line in ( ): is the library’s termination detection: it resolves when the queue is empty and no task is in flight. Same idea as muffet’s and LinkChecker’s , just expressed as a promise and backed by a single-threaded runtime, so no Mutex is needed to protect the visited set. The back-edge and the race-free dedup When crawling, GETs the page, extracts links, and for each new URL re-enters the queue ( ): Because JavaScript is single-threaded, the entire thing executes without interruption. In Rust or Go, that’s a critical section you must guard with a mutex (and get the ordering right); in Node it’s just three statements. This is the single biggest reason recursion is easier in Node than in Rust. It’s just a language feature. linkinator also keeps a of keys, and a map so it can wait on an in-flight check and still report a duplicate broken link against every parent that references it. Those reuse-operations are themselves pushed onto the same queue, so correctly waits for them too. HEAD vs GET linkinator uses for leaf links but when it needs to crawl, because recursion needs the response body to find more links : This is precisely lychee’s remaining open problem : you can only recurse into pages you fetched with a body. linkinator just always GETs when crawling; lychee plans to reuse the body it already has in cache from the check it just performed. Where the tradeoffs are Single-threaded is both a blessing and a ceiling. No data races, trivially correct dedup, but HTML parsing is CPU work that blocks the one event loop. For thousands of pages, you’re bound by a single core. lychee’s multi-threaded runtime parses and checks in parallel. It suffers from in-memory result inflation. The source explicitly comments on “massive result inflation for heavily interlinked sites”: the array, , and all grow with the crawl. Fine for a docs site, heavy for a giant one. Rate limiting is reactive, not proactive. There’s a that backs off per host on a with , but no general per-host concurrency cap like lychee’s . linkinator can hammer a host until it complains; lychee now paces before the complaint. For extensibility, it’s an ( , , and so on), so it’s embeddable and scriptable, which is nice. It’s a library first, like lychee. Takeaways: linkinator broken-link-checker (JavaScript): event-driven, using two queues broken-link-checker (BLC) takes the event-driven model furthest. It’s built on , a queue with (concurrency) and , and it nests two of them: a site-level queue feeding a page-level . The frontier and dedup live in ( ). Visited pages are tracked in a , written at enqueue time: Recursion is governed by a filter that decides whether a discovered link becomes a crawled page: Termination by event cascade BLC has no counter and no . It rides the queue’s drain events. When the page-level queue empties it fires , which makes emit and call the site queue’s callback; when the site queue drains, it fires . That’s the public : That’s their termination detection, expressed as “the request queue reported empty.” And in classic Node.js fashion, the callback is what actually tells the site queue to free up a slot for another site. So the termination of one site is what allows another to start, and the termination of the whole crawl is what allows the process to exit. It’s a cascade of events that propagates from the page queue to the site queue to the process. Where the tradeoffs are It’s the best web citizen of the bunch. robots.txt is honored ( , ), is respected, and plus are first-class. This is a crawler that’s polite by default. Event cascades are powerful but fiddly. Termination is spread across half a dozen event handlers and two nested queues. It works, but the control flow is much harder to follow than . This is the JS cousin of the “leaky abstraction” problem I described, where recursion-awareness ends up sprinkled across many handlers. It’s single-threaded, the same ceiling as linkinator, plus the in-memory per site. On maturity versus momentum, it’s very widely used (it powers a lot of tooling), but development has slowed. The architecture is still sound and worth studying. Takeaways: broken-link-checker A note on markdown-link-check and the “industrial” crawlers Our README marks markdown-link-check as supporting recursion, but there’s some nuance there: it recurses over Markdown files , not by spidering a live website. There’s no HTTP frontier and no termination problem in the sense above. Worth a mention so the comparison is honest, not worth a teardown. If you want to see the pattern at full industrial scale, look at Scrapy (Python/Twisted) or Colly (Go). Both use the same approach: a scheduler (frontier) with a pluggable, optionally disk-backed queue, a dupefilter (often a Bloom filter rather than a ), a bounded downloader pool, and explicit “engine idle → close spider” termination. They solve exactly the problems lychee struggled with ( distributed termination detection , backpressure, dedup), just with years of dedicated crawler engineering behind them. The takeaway isn’t “lychee should be Scrapy”: it’s that crawling is a well-trodden architecture, and lychee is simply standing on a different one right now. Side-by-side Tool Lang / runtime Concurrency model Frontier “Done?” signal Dedup point Per-host limiting muffet Go, goroutines goroutine pool + semaphore + host throttler mutex-guarded set + daemon channel visited set at enqueue host throttler pool LinkChecker Python, threads fixed blocking thread pool unbounded joinable-queue counter ( ) result cache at (req/s) linkinator Node, event loop single-thread + p-queue ( ) p-queue at enqueue (race-free) reactive broken-link-checker Node, event loop ( ) nested request queues queue-drain events at enqueue + lychee (2026) Rust, Tokio tasks + channels + per-host pool lychee in 2026 finally has a column-for-column match. The is muffet’s and LinkChecker’s . The is BLC’s / and LinkChecker’s . The per-URI mutex is everyone’s enqueue-time dedup. So Why Couldn’t We Just Copy Them? Three reasons, in increasing order of how much they’re actually lychee’s fault. They started as crawlers; lychee started as a stream. Every tool above has a back-edge in its core data structure. lychee’s core was a DAG optimized for the 99% case (a list of files/URLs, checked once, fast). Retrofitting a cycle onto a pipeline is much harder than having one from the start. The problem is architectural in nature. The frontier and the rate-limiter must be different objects. muffet (set + semaphore), LinkChecker (unbounded queue + thread count), linkinator (p-queue + delayCache), BLC (request queue + maxSockets) all keep “what to do next” separate from “how fast to go.” lychee’s early attempts tried to make one bounded channel serve both roles, and a cycle through a bounded channel deadlocks. The fix (lychee’s plus a over an unbounded work source) is the same separation we’re aiming for now. Single-threaded runtimes get dedup for free. Both Node tools dedup with a plain and zero locking, because the event loop serializes access. Go and Python pay a mutex. Rust pays a mutex and fights the borrow checker about who owns the shared state across . That’s the ~30% “Rust tax” I estimated last time : not the algorithm, but the friction of expressing shared mutable frontier state under . None of this is a knock on lychee’s design. A unidirectional stream is the right call for the common, non-recursive case: it’s why lychee is fast and why the 30% channel regression from Attempt 2 was a dealbreaker. The other tools pay for their back-edge on every run, recursive or not. lychee refused to, and that principle is exactly why recursion took five years and why, when it lands, it won’t slow down the path everyone actually uses. I believe that we can have our cake and eat it too: a crawler architecture that supports recursion without sacrificing the speed of a one-shot pipeline. But it’s a harder problem than just “copy what they do,” because most link checkers didn’t start with uncompromising performance as their top goal. Key takeaways So when someone asks “how do other link checkers do recursion?”, the real answer is: they made it a part of the architecture from the beginning, and they leaned on a runtime (providing conveniences like a , a joinable queue, an idle promise) that solved termination without solving “distributed termination detection.” Thanks to the maintainers of muffet, LinkChecker, linkinator, and broken-link-checker: reading your source is the clearest way to learn about crawler architecture out there and we’re all in this together, just with a different set of tradeoffs. A mutable work queue (let’s call it “frontier”), not a fixed input stream. Discovered URLs go back into the same queue they came from. A visited set that’s updated at enqueue time (before the request completes), so two pages discovering the same link can’t both submit it. A primitive that answers “is everything done?”: a , a joinable-queue counter, an promise, or a queue-drain event. Concurrency isn’t bounded by the daemon manager. does for every task, spawning unbounded goroutines. The actual limiting happens downstream in a (a buffered-channel counting semaphore) and a per-host throttler pool. muffet separates “the frontier” from “the rate limiter,” which is exactly the separation lychee lacked when it tried to use one bounded channel as both in the past. Cheap goroutines do a lot of heavy lifting. Spawning a goroutine per link is “fine” in Go. The equivalent in Rust ( per link, each needing state) is what pushed me toward and the ownership pain I wrote about . On extensibility, muffet is a focused CLI, not a library. There’s no plugin surface; you get what the flags give you. lychee deliberately ships as a reusable crate, which raises the bar, since every architectural choice has to uphold the standards of a public API. On scalability, unbounded goroutines plus an in-memory visited set scale comfortably to large sites, but there’s no disk-backed frontier, so a truly enormous crawl is bounded by RAM. Same as lychee. muffet’s termination is a , full stop. It’s the design lychee converged on after five years; muffet got it for free from Go’s standard library on day one. The frontier and the concurrency limiter are separate things. A mutex-guarded set is the frontier; a semaphore plus host throttler bounds concurrency. Conflating them is what deadlocked lychee. Goroutines hide the cost that Rust makes you pay explicitly. The same per-task model that’s trivial in Go is where Rust’s /ownership friction shows up. Blocking threads instead of async. Each of the (default 10–100) threads does blocking I/O via . Simple and battle-tested, but the concurrency ceiling is the thread count, and each thread carries a full stack. lychee’s Tokio model reaches thousands of concurrent in-flight requests on a handful of OS threads; LinkChecker can’t, and doesn’t try. The unbounded frontier trades a deadlock for unbounded memory. The explicit “no max size” decision means RAM growth on huge sites. There’s a cap and a periodic to mitigate it. Extensibility is excellent. LinkChecker has a real plugin system ( : anchor checks, SSL, virus scanning, and more) and many output loggers. This is the most extensible of the bunch, and it pays for that with a large, mature, somewhat old-fashioned codebase. On scalability, it’s GIL-bound and thread-limited, so raw throughput is the lowest here, but correctness and feature coverage are high. The unbounded frontier is a deliberate anti-deadlock choice, documented in a one-line comment. It describes the exact problem we hit in lychee in attempt 4. Dedup at time (a placeholder in the cache) is their synchronization mechanism. The cache must claim the URL before the request, not after. Threads buy simplicity at the cost of throughput. A blocking thread pool is the easiest correct model… and the slowest one. Single-threaded is both a blessing and a ceiling. No data races, trivially correct dedup, but HTML parsing is CPU work that blocks the one event loop. For thousands of pages, you’re bound by a single core. lychee’s multi-threaded runtime parses and checks in parallel. It suffers from in-memory result inflation. The source explicitly comments on “massive result inflation for heavily interlinked sites”: the array, , and all grow with the crawl. Fine for a docs site, heavy for a giant one. Rate limiting is reactive, not proactive. There’s a that backs off per host on a with , but no general per-host concurrency cap like lychee’s . linkinator can hammer a host until it complains; lychee now paces before the complaint. For extensibility, it’s an ( , , and so on), so it’s embeddable and scriptable, which is nice. It’s a library first, like lychee. is the termination mechanism. Simple and provided by the JS runtime. A single-threaded event loop makes request deduplication pretty much free. This is the biggest structural reason recursion is easier in that case. Reactive 429 backoff is not the same as proactive per-host pacing. lychee’s aims higher, at the cost of more machinery. It’s the best web citizen of the bunch. robots.txt is honored ( , ), is respected, and plus are first-class. This is a crawler that’s polite by default. Event cascades are powerful but fiddly. Termination is spread across half a dozen event handlers and two nested queues. It works, but the control flow is much harder to follow than . This is the JS cousin of the “leaky abstraction” problem I described, where recursion-awareness ends up sprinkled across many handlers. It’s single-threaded, the same ceiling as linkinator, plus the in-memory per site. On maturity versus momentum, it’s very widely used (it powers a lot of tooling), but development has slowed. The architecture is still sound and worth studying. Termination is a cascade of queue-drain events, not a counter. Same idea, different syntax. Politeness is built in. robots.txt, , and make it the most server-friendly recursive checker by default. Event-driven control flow is the cost. Distributing recursion logic across many handlers is exactly the kind of spread-out complexity that makes the feature hard to reason about. There is no secret sauce. Every recursive checker is a worklist plus a visited set plus a quiescence detector. The “trick” is being shaped like a crawler from commit one. Termination is always the same idea wearing different clothes: (muffet), joinable-queue counter (LinkChecker), (linkinator), queue-drain events (BLC), (lychee 2026). All of them are distributed termination detection. Dedup belongs at enqueue, before the request. Marking a URL visited after checking it (what lychee did for four attempts) is the bug. Everyone else claims the URL the moment it enters the frontier. Separate the frontier from the rate limiter. A bounded channel that is both your queue and your backpressure will deadlock the instant you add a cycle. There is no free lunch. Node’s single thread makes dedup trivial at the cost of performance; Go’s goroutines and make termination trivial at the cost of a runtime; Rust gives you neither for free but hands you a compiler that refuses to let the races compile and you can get the network card to glow if you know exactly what you are doing.

0 views
iDiallo 4 days ago

The web is changing, and we are not going back

Whenever I saw someone type a natural language query into Google, it made me cringe. "It's not a person," I would say. "Type like you're talking to a machine." This was especially true for programmers and it was before AI took over everything. Instead of "how do I write a function that reads a file?", I would suggest they use specific keywords, something that sounded more like machine language than conversation. "js function to read csv file" or "css gradient background property example." This got you better results. Even though Google was a sophisticated search engine, it was still doing a kind of keyword matching under the hood. But not anymore. You don't get any advantage from writing in "machine language." Google understands natural language just as well. In fact, even better. How is it that in 2026, I Google things less than ever? It's not that I know everything now. It's more that I don't want to call the friend who always talks too much. If the height of the Eiffel Tower ever comes up in conversation, I'll type "eiffel tower wiki" and click through to Wikipedia. I don't want to have a conversation about it. Googling something these days feels like Google is trying to join my private conversation. Where it used to be a tool for finding answers elsewhere, now it's a buddy who gives you an answer. And just as you're about to leave, it says, "hey, did you also know that..." There used to be a machine between me and the information I was looking for. It was good at its job. It sorted, ranked, then presented information. But now, the machine is constantly pushing information at me, watching my reaction, learning from it, and feeding me more, unsolicited. Before, information lived on the web and was hard to find. Today, information still exists, but it's buried under noise. Google no longer helps you find it, it just gives you an answer. That answer might be right or wrong, and right below it, in small print: "AI responses may include mistakes." You rarely get to verify whether the answer is correct, because almost no one clicks through to the source. I know this firsthand. More than three-quarters of my Google referral traffic has disappeared, while my search impressions keep climbing. So what's left to do? I could mourn the old Google, the simpler web. But as the title says, we aren't going back. This is the new reality, and we have to adapt. Rather than blindly embracing change, I think it's smarter to pick and choose. Just last week, I wrote about the small web still being alive . And it did exactly what its name suggests. It stayed small. There are other search engines built for people who want more control. DuckDuckGo. Kagi (my personal favorite). The habit of Googling everything is learned behavior and learned behaviors can be unlearned. What's harder to convey is that Google never presented us with facts, only sources and citations. The way the google answer is presented, we have the impression they are giving us undisputable truths. When everyone is sharing screenshots of the answer they got, all you can do is share a screenshot of the opposite answer you got. The source gets lost. That's where we are now. Skimming the average sentiment of a Reddit thread, or confirming something we already believed. This is the new reality. We're not going back to keyword matching. But I also don't have to accept the new way as the only way. Google has made its search box AI-first and that's their right, it's their product. But it's also my opportunity to try something different. We are not going back. So I might as well choose where I go next.

0 views
Xe Iaso 5 days ago

"No way to prevent this" say users of only package manager where this regularly happens

In the hours following the news that Redhat Insights' JavaScript packages fell victim to a supply chain attack via NPM, developers and systems administrators scrambled ensure all of their projects were unaffected from a supply chain attack that steals credentials for AWS, GCP, Azure, Kubernetes, HashiCorp Vault, npm, and CircleCI before then self-propagating via said stolen npm credentials and the bypass_2fa setting. This establishes persistence via Claude Code hooks and VS Code task injection. If you have installed the affected package, reprovision your development hardware. This is is due to the affected dependencies being distributed via NPM , the only package manager where these supply-chain attacks regularly happen. "This was a terrible tragedy, but sometimes these things just happen and there's nothing anyone can do to stop them," said programmer Lady Eulah Howell, echoing statements expressed by hundreds of thousands of programmers who use the only package manager where 90% of the world's supply-chain attacks have occurred in the last decade, and whose projects are 20 times more likely to fall victim to supply chain attacks. "It's a shame, but what can we do? There really isn't anything we can do to prevent supply-chain attacks from happening if the maintainers don't want to secure access to their accounts in a robust manner". At press time, users of the only package manager in the world where these vulnerabilities regularly happen once or twice per week for the last year were referring to themselves and their situation as "helpless". For more information, please see upstream documentation published by Redhat Insights' JavaScript packages at the following link: redhat-javascript-clients-06-2026 .

0 views
Sean Goedecke 5 days ago

Weird projects I shipped with AI

Where are all the AI-generated projects? This is a common question from AI skeptics: if LLMs are so good at writing code, where is the tsunami of new AI-generated apps, services and games? I personally don’t find this to be much of a paradox. Writing code is only one of the bottlenecks involved in actually shipping a new product, after all. It’s also impossible to talk about the paid work I’ve done with AI (you’ll simply have to take my word that it’s increased my productivity). But one thing I can do is share a list of personal projects I’ve built with AI in the last twelve months. I definitely would not have done all of these by hand. I might have found the time to do one or two of them, but based on my pre-AI track record they would probably have stayed in the “GitHub repo with a few commits” stage. This list is a kind of existence proof : a bunch of weird projects, useful to at least some people, that would not have existed without AI assistance 0 . Most recently I’ve built skifreedle.com , a daily-game version of the classic Windows SkiFree game (i.e. “like Wordle, but for SkiFree”). The code for that is here 1 . I enjoy coding small web games by hand, but definitely would not have had the time to wire up all the different SkiFree objects or build neat features like a ghost of your fastest run. I also tried out a lot of different visual themes for the game UI before landing on something I liked. If I’d done this by hand, I would have only had time to try out two or three different looks, instead of fifteen or twenty. I’m very happy with how this turned out. I’ve been enjoying competing against my brother to get better times, since both of us have a lot of nostalgia for the original SkiFree game. Last year I built Autodeck ! I wrote a blog post about this before, but this came from my partner wishing there was some way to automatically generate Anki cards about random topics she wanted to learn about. It ended up being relatively straightforward to set up an endless feed of auto-generated spaced repetition cards: I set up Stripe payments for this one, more because I was worried about someone running away with my Groq balance than because I wanted to make money, but I was pleasantly surprised to see a bunch of people actually use this. Over five hundred people have tried it out, with enough paid subscribers to cover inference and hosting. I might have built this without LLM assistance, but I almost certainly would not have deployed it as a website. The hassle of setting up a database and Stripe would have just been too much work. I also built an AI-generated endless wiki . I wrote a blog post about this one as well. Like Autodeck, I was fascinated with the idea of non-chat interfaces for LLMs, and I thought a wiki-based approach where you interact with the model by clicking links was pretty cool. I learned the hard way that putting a LLM generation call on the end of a regular link was a bad idea: scrapers would exhaust my inference budget quickly. I ended up faking the no-article-exists-yet links with JavaScript, which at least so far has defeated scrapers. People still email me about Endless Wiki, and there are over 280 thousand pages generated. My original goal was to see if you could eventually generate a page for Neon Genesis Evangelion, starting at the root page and only following links (kind of like wiki golf ). I was successful! You can read the “Evangelion Anime” page here . Almost exactly a month after I launched Endless Wiki, xAI launched Grokipedia . Obviously they didn’t plagiarize me. This is a very easy idea to have, and my site was not the first infinite wiki (though I think it was the first one where you had to discover new pages by clicking on links). But it did take some of the shine off. I built a PWA that caches the VicFlora plant identification database so it could be used with low or no internet. This was more of a utility project for my partner, who likes plants and occasionally goes on field trips where internet is spotty. I would definitely not have done this without LLMs. It was reasonably difficult to scrape the basic dichotomous key from the VicFlora website: their API documentation was out of date, there were multiple possible pathways for fetching data (most of which were not functional), and the format of the data I did manage to fetch was hard to parse. I think I could have done it, with enough effort, but it would have been a substantial amount of work. I’m very happy with how this turned out. It’s not perfect, but it’s functional, and I’ve even had the occasional Victorian botanist email me with bug reports or feature requests, so it’s clearly seeing a little bit of usage. I did a bunch of other stuff that doesn’t necessarily rise to the level of a “deployed project”: my gh-standup GitHub CLI extension to automatically generate a standup report, which has just over a hundred stars, my (low quality) image geolocation benchmark , which I blogged about here , or my skill for extracting features from open-source models. There may not be a flood of AI-generated companies (yet), but at least for me there’s been a flood of small, weird projects that would not have existed without significant LLM assistance. I also want to shout out Simon Willison’s version of this , which is another great example of “weird useful tools that only exist because the cost of creating them was so low”. I did lift the spritesheet from DanielHough’s SkiFree.js , which attributes it to Wing Wang Wao . Of course, the original sprites and art belong to Chris Pirih’s SkiFree and Microsoft. I also want to shout out Simon Willison’s version of this , which is another great example of “weird useful tools that only exist because the cost of creating them was so low”. ↩ I did lift the spritesheet from DanielHough’s SkiFree.js , which attributes it to Wing Wang Wao . Of course, the original sprites and art belong to Chris Pirih’s SkiFree and Microsoft. ↩

0 views
JSLegendDev 1 weeks ago

My Biggest Gripe With YouTube

3 years ago, I started a YouTube channel called JSLegendDev where I uploaded tutorials teaching the JavaScript programming language through the development of 2D games. The state of the space around the time I started was as follows : Tutorials inferior to an hour in length were not in demand. They made very little views. Tutorials divided into multiple parts where dead on arrival. You were guaranteed dwindling views on every new upload. To adapt, other content creators started uploading longer, multi-hour, often project based tutorials which translated to more views. Seeing the shift, I also decided to follow suit and uploaded tutorials reaching the 4-10 hour mark. I saw some success doing this. Therefore, I kept at it for a while. However, as time passed, I got tired of recording extremely long tutorials and they, in general, started to make less views. There are many hypotheses as to why YouTube’s algorithm started serving tutorial content less. The advent of AI could’ve been the likely cause but also a general shift in YouTube becoming more of an entertainment focused platform to the detriment of educational content. Something you now put on TV to relax. In the programming space, channel producing content that can be watched passively like tech news, tech drama, tech history, high level discussions, etc… continued to thrive. Seeing this new shift and because I was genuinely tired of making YouTube tutorials, I published my first scripted video titled “How do Devs Make Levels Without Game Engines” which was first published as an article. In that piece, I told the story of how I discovered a convenient way to design levels for my games using an external editor called Tiled in conjunction with my editor-less game framework. At the end of that video, I promoted a paid tutorial I made teaching the exact steps needed to achieve what was presented. The video ended up accumulating over 30k views, which was pretty great! It took far less effort to make compared to my multi-hour tutorials and I was able to make a few sales on my paid tutorial I mentioned within. Previously, I was very unsuccessful in selling any paid courses and I didn’t quite understand why. However, the answer now hit me like a truck. Why would anyone still have the appetite for a paid course after having invested the time following a free multi-hour course? Even if the subject of the paid offering was different, they would probably be too tired to commit to another one. Anyway, following in the footsteps of this first breakthrough, I uploaded another scripted video titled “You Can Now Make PS2 Games in JavaScript” which was again first published as an article. In that video, I told the story of how I discovered that you could make PS2 games in JavaScript and provided an overview of how the viewer could get started. Despite including very practical knowledge, the viewer was never expected to follow along and therefore could watch it passively. It was a resounding success, over 100k views! Unfortunately, I didn’t sell any courses in that video because I simply didn’t have the energy to both make the video and a course. The best business decision would have been to wait before uploading. I’ll go into more details later, but my biggest gripe with YouTube is that it’s no longer a great platform to build an audience but rather it’s only good for reach and here, I had wasted a lot of reach. After having made so many game development tutorials, I wanted to try my hand in creating an original game that I would sell on Steam. Once the project was starting to take shape, I had the idea of making a video about it to gauge interest as I wasn’t sure it would find an audience. Therefore, I had the idea of using the same format used in my two previous successful videos. However, rather than focusing on technical details, I instead would tell the story of how I came up with my game’s design covering the various iterations and challenges I faced while working on it. Therefore, I ended up uploading a video titled “Making a Small RPG” which again, was originally an article. It was also a resounding success reaching barely below 100k views! However, it came with a hidden cost. That cost was the tipping point that made me realize that YouTube is no longer a good platform to build an audience on. I naively thought that if the video performed well, this would translate to subscribers and an audience eager to hear more about the project, but this wasn’t the case. I had made a big mistake by not setting up a Steam page to direct viewers to before publishing the video. On my next upload concerning the project, the fall off in terms of views was brutal. I went from 98k views to below 10k. It became clear that YouTube was acting as a gatekeeper between me and the audience I thought I had built. After reflecting on the situation, I came to the following conclusion. The reason my 3 previous videos had performed well was due having certain characteristics that aligned with YouTube’s goal as a platform, which consists in making people watch videos for as long as possible so they can serve more ads. I listed them below : The subject of all three videos were remarkable which lead to people clicking on them. Something is remarkable when it obviously stands out as being interesting/noteworthy. For example, the subject of my video titled “You Can Now Make PS2 Games in JavaScript” is remarkable because the PS2 is a very popular, but now old console and you had to use a hard programming language called C++ to make games for it. Being able to now use JavaScript, a simpler but most importantly, a language originally designed for making websites and not games, makes the subject come across as immediately noteworthy. Therefore, remarkable. The use of storytelling made people eager to watch more of the video. This can be explained by the fact that we instinctively want to know what happens next in a compelling story. Finally, the length of the videos were all above 10 minutes and the 2 more successful ones were in the 15+ min range. This resulted in more absolute watch time compared to shorter content. For example, if 2 videos are both watched fully by the same audience. The shorter one will translate to less total time spent on the platform compared to the longer one. Therefore, YouTube will recommend the longer one instead because there’s an opportunity cost to doing otherwise. To understand the fall off, it’s important to first mention that usually, series on YouTube don’t work. The second video of a series ends up making less views than the first because it requires prior context before clicking. Thus reducing its appeal and limiting its reach. However, I knew this going in. I tried making the second video as independent as possible but in the end, a second video talking about the same subject was bound to be less remarkable. It didn’t help that because I summarized the content of the first video in the second one, a familiar viewer would have found it less engaging making the video further away from hitting criteria 2 and 3 that I outlined above. Consequently, I realized I had wasted my biggest marketing ammunition regarding my small RPG game as I had no way to contact the audience hit by the first video. Like with the one on making PS2 games in JavaScript, I had wasted tremendous reach. At this point, I realized my biggest gripe with YouTube was simply that I could not access my audience reliably. Therefore, was it really my audience? On one hand, YouTube allows someone without a following to reach millions but on the other, the link to those reached is fickle. I thought I was building an audience by gaining subscribers but instead, I was building a sand castle that could easily be carried away by the slightest algorithm waves. YouTube wasn’t always like this. People used to subscribe to channels and seek their content in their subscriptions tab. However, the platform effectively buried this model by conditioning users to seek recommended videos on the home page and deprioritizing the Subscriptions tab to the point that it barely looks like a clickable section. You have to click on the “Subscriptions” text to access your sub feed. Doesn’t look very clickable doesn’t it? I think that we’re now entering an era where YouTube is starting to treat content creators as interchangeable much like TikTok. They saw the success TikTok had, tried to replicate it with Shorts and now YouTube long form is getting affected as well. I fear that in the future, uploading to YouTube will look no different than making posts on Reddit. You might get views, you might get comments, but they’re self contained to a specific post with no following building up and no guarantee of your next posts having the same reach. The conclusion to all of this is that it’s not worth it to be a YouTuber. Relying on YouTube adsense and sponsorships (sponsors use views as a metric to determine how much to pay you) for your livelihood is simply not sustainable due to how fickle getting views on the platform is. Therefore, focusing so much on making YouTube content will most likely lead to your exploitation. That said, is quitting really the answer? Considering that YouTube can give you incredible reach even if you’re a nobody as long as you make content that is remarkable, engaging (for example, through storytelling) and long enough, it would be stupid to completely walk away, at least in my case. Therefore a new strategy appears on the horizon. It consists in building your audience outside of YouTube through a mailling list (Substack conveniently allows you to do so) and to strategically make occasional compelling YouTube content to tap into the platform’s reach potential. However, the key is to always direct viewers to the mailling list. Why is building an audience through email so important? because it allows you to have a direct and long lasting link with your audience. It also gives you independence from social media platforms. Even in the case of Substack, where this article is currently hosted, I can export my email list and move to another platform or email sending service without my subscribers even noticing. This shift implies that I no longer need to worry about pumping frequent content for YouTube because I’m not making money through them or worrying about doing so. By making YouTube content rarely, I get to keep most of my energy to build something compelling outside the platform like an actual game, writing interesting articles, making an in-depth course or other kinds of art/products. This plan seems to me as more sustainable and more healthy long term. That’s about all I’ve got to share. Hope this article was insightful. If you’re curious to see where this journey will lead, I recommend subscribing! I usually write about programming, game development and game design. Subscribe now You can check some of my previous articles below. Tutorials inferior to an hour in length were not in demand. They made very little views. Tutorials divided into multiple parts where dead on arrival. You were guaranteed dwindling views on every new upload. The video ended up accumulating over 30k views, which was pretty great! It took far less effort to make compared to my multi-hour tutorials and I was able to make a few sales on my paid tutorial I mentioned within. Previously, I was very unsuccessful in selling any paid courses and I didn’t quite understand why. However, the answer now hit me like a truck. Why would anyone still have the appetite for a paid course after having invested the time following a free multi-hour course? Even if the subject of the paid offering was different, they would probably be too tired to commit to another one. Anyway, following in the footsteps of this first breakthrough, I uploaded another scripted video titled “You Can Now Make PS2 Games in JavaScript” which was again first published as an article. In that video, I told the story of how I discovered that you could make PS2 games in JavaScript and provided an overview of how the viewer could get started. Despite including very practical knowledge, the viewer was never expected to follow along and therefore could watch it passively. It was a resounding success, over 100k views! Unfortunately, I didn’t sell any courses in that video because I simply didn’t have the energy to both make the video and a course. The best business decision would have been to wait before uploading. I’ll go into more details later, but my biggest gripe with YouTube is that it’s no longer a great platform to build an audience but rather it’s only good for reach and here, I had wasted a lot of reach. After having made so many game development tutorials, I wanted to try my hand in creating an original game that I would sell on Steam. Once the project was starting to take shape, I had the idea of making a video about it to gauge interest as I wasn’t sure it would find an audience. Therefore, I had the idea of using the same format used in my two previous successful videos. However, rather than focusing on technical details, I instead would tell the story of how I came up with my game’s design covering the various iterations and challenges I faced while working on it. Therefore, I ended up uploading a video titled “Making a Small RPG” which again, was originally an article. It was also a resounding success reaching barely below 100k views! However, it came with a hidden cost. That cost was the tipping point that made me realize that YouTube is no longer a good platform to build an audience on. I naively thought that if the video performed well, this would translate to subscribers and an audience eager to hear more about the project, but this wasn’t the case. I had made a big mistake by not setting up a Steam page to direct viewers to before publishing the video. On my next upload concerning the project, the fall off in terms of views was brutal. I went from 98k views to below 10k. It became clear that YouTube was acting as a gatekeeper between me and the audience I thought I had built. After reflecting on the situation, I came to the following conclusion. The reason my 3 previous videos had performed well was due having certain characteristics that aligned with YouTube’s goal as a platform, which consists in making people watch videos for as long as possible so they can serve more ads. I listed them below : The subject of all three videos were remarkable which lead to people clicking on them. Something is remarkable when it obviously stands out as being interesting/noteworthy. For example, the subject of my video titled “You Can Now Make PS2 Games in JavaScript” is remarkable because the PS2 is a very popular, but now old console and you had to use a hard programming language called C++ to make games for it. Being able to now use JavaScript, a simpler but most importantly, a language originally designed for making websites and not games, makes the subject come across as immediately noteworthy. Therefore, remarkable. The use of storytelling made people eager to watch more of the video. This can be explained by the fact that we instinctively want to know what happens next in a compelling story. Finally, the length of the videos were all above 10 minutes and the 2 more successful ones were in the 15+ min range. This resulted in more absolute watch time compared to shorter content. For example, if 2 videos are both watched fully by the same audience. The shorter one will translate to less total time spent on the platform compared to the longer one. Therefore, YouTube will recommend the longer one instead because there’s an opportunity cost to doing otherwise.

0 views
Manuel Moreale 1 weeks ago

AI blog question challenge

Rishabh emailed me the other day, asking me to answer the 7 questions of his new blog challenge, and who am I to say no to such a request? So here we go. I assume by AI models we mean the current crop of LLMs, and not AI models in general, because I’m old enough to remember when “Machine Learning” was a thing. What even is AI anyway at this point, since everything is lumped together into one useless definition? Anyway, I believe my first experience was trying out chagpt back when it first came out. I don’t think I spent more than 10 or 15 minutes using it at the time. It was impressive tech, but was also completely useless for me at the time, and that’s why I didn’t bother spending more time using it. This is an interesting question. Do I use AI? Well, I guess the answer is yes since it’s almost impossible to avoid using it if you use the web at this point. Pretty much all tools and services are integrating some sort of AI-powered functionalities, and it’s become harder and harder not to use them. If, instead, the question is do I use one of the various LLMs directly to do stuff, then the answer is still yes, but the amount of usage is so low that some people might consider that to be the same as not using them at all. I don’t directly pay for any of the models, but my work email has been powered by Google for more than a decade, and so I do get access to Gemini Pro. Workspace has usage data for everything, and I just looked it up: In the last 90 days, the only AI-related feature I used was the Gemini App (that’s not surprising considering I turned off everything else), and I have apparently used it 62 times. I’m now looking at the history of those chats, and pretty much all of them are single-question queries related to something web dev I was doing. Things like how to do a specific thing inside Kirby, or how to achieve something using a particular JS library. This is stuff one should be able to find inside documentation websites, but the search there is often awful and so after a google search, I try my luck with AI. And as I wrote somewhere else, I never copy-paste. I ask very narrow questions so that I can be pointed towards the correct answer. And once I have that, I do the coding and I re-implement everything myself. Am I against using AI? As a generative tool, yes. I refuse to ask AI to do something for me or to generate content from scratch. As a tech in general? I think it has some potentially useful applications in narrow contexts. As always, the answer is not cut-and-dry, and it can be yes or no depending on the framing and the scope. The only aspect I appreciate is the ability to ask questions in natural language. Because sometimes you have a problem or an answer you’re looking for that can’t be described in a more structured way. As for what I don’t like, how anthropomorphised these stupid tools are is definitely high on my list. I don’t want my computer to talk back or to make jokes or to say «I’m sorry». If I input a question, I want an answer back, and that’s it. I don’t want follow up questions, I don’t want some pointless preamble. I get why this happens, but I fucking hate it. This is software. I don't want my software to have a personality. I want it to perform a task and get out of my way. I also don’t like the lying, the gaslighting, and all the other crap, and I also don’t like what the AI industry is doing as a whole, but that’s a separate issue. Again, another question that has different answers depending on the scope. The idea of being able to generate images, in general, is neutral to me. It all comes down to what you use it for. There are some potential use cases that are totally fine, others are completely insane. As a whole, I think the ability to generate slop is bad, but that’s because humanity can’t be trusted to do anything the right way. As for their use in blog posts, I think stock images were useless, and I don’t see images generated with AI to be any different. Unless you have generated an image as part of the content to explain or visualise something. That’s fine, label it as a generated image and move on. That’s no different than including a render, or a sketch on paper, from a content perspective. My consumption of online content these days is so limited that I don’t have this issue. I read very few blogs, and I know they are not AI generated because I emailed the people behind them more than once, and I know what their stance is. I watch almost no YouTube, and I only read a few news sites. My strategy is to simply stay away from the digital world as much as possible, and I’m at the point where I’m considering dropping my digital consumption down to zero and quit the internet as a place for content. I have zero hope. And that is because I have zero hope in anything that’s in the hands of mega corporations. The incentives are totally skewed, and they’d do everything they can in order to keep the line go up. I don’t see people with strong morals in positions of power and so unless we decide to go full French Revolution, I see no reason for things to improve. Thank you for keeping RSS alive. You're awesome. Email me :: Sign my guestbook :: Support for 1$/month :: See my generous supporters :: Subscribe to People and Blogs

0 views
Evan Hahn 1 weeks ago

Notes from May 2026

My blog turned 16 this month! I did nothing to celebrate, but made some little tools and clicked some links about tech ethics. I published four little tools this month: I also did some work on Helmet, my open source project: And like every month, I wrote a few articles at Zelda Dungeon . I don’t feel I wrote anything special this month, but my colleagues put together a feature about Zelda and mental health which was very affecting! “The vast majority of tech workers, at least those who I have encountered in my many years of reporting, are not vampiric Silicon Valley tech bro caricatures [… They] both like working with tech and ultimately want to see it serve the public good.” From “They just formed the biggest tech worker union in the US. They plan to rein in AI and curb layoffs” . This “love letter to Gnutella” is both an introduction to a P2P protocol and a celebration of the culture around it. From “Affordances for me, but not for thee” : “One of the oddest parts of the AI shift is that people are much more willing to do things for LLMs that they should have been doing for human beings all along.” Accessibility, specifications, documentation, and policies are better codified now. The author calls this “dystopian”, and I agree: our motivation to do this stuff is AI or productivity, not helping our fellow human. “More importantly, whereas accessibility affordances provide new abilities for vulnerable people, an AI affordance provides new abilities for people with power. And that’s probably the heart of it.” Looking forward to being surveilled because I’m an “anti-tech extremist” . I can’t tell you how exciting it was to watch Jira add 2 + 3 . “What can I do to resist AI?” asks the AI Resist List . “Tech companies like Google, Facebook and Microsoft are ignoring data controls mandated under California law, researchers say.” “Your AI Slop Bores Me” presents an interface that looks like an LLM chatbot, but it’s entirely powered by humans. A very cute idea. I’m a very bad “image generator”, at least according to the ratings I received. I continue to be amazed by “Lest We Forget the Horrors: An Unending Catalog of Trump’s Cruelties, Collusions, Corruptions, and Crimes” . It’s so thorough. RIP to a real one: Wikinews is shutting down after 21 years . Hope you had a good May. ZIP Shrinker , a web app that shrinks ZIP files with higher compression ratios A command line tool to do (completely offline) translation Open Link in Unloaded Tab , a Firefox extension to open links without loading them png-cmp , a command line tool to compare PNG pixel data After over a year of quiet maintenance, I released version 8.2.0 with some small new features and documentation updates. In a step toward dropping GitHub, I moved the docs from a GitHub URL to helmet.js.org . “The vast majority of tech workers, at least those who I have encountered in my many years of reporting, are not vampiric Silicon Valley tech bro caricatures [… They] both like working with tech and ultimately want to see it serve the public good.” From “They just formed the biggest tech worker union in the US. They plan to rein in AI and curb layoffs” . This “love letter to Gnutella” is both an introduction to a P2P protocol and a celebration of the culture around it. From “Affordances for me, but not for thee” : “One of the oddest parts of the AI shift is that people are much more willing to do things for LLMs that they should have been doing for human beings all along.” Accessibility, specifications, documentation, and policies are better codified now. The author calls this “dystopian”, and I agree: our motivation to do this stuff is AI or productivity, not helping our fellow human. “More importantly, whereas accessibility affordances provide new abilities for vulnerable people, an AI affordance provides new abilities for people with power. And that’s probably the heart of it.” Looking forward to being surveilled because I’m an “anti-tech extremist” . I can’t tell you how exciting it was to watch Jira add 2 + 3 . “What can I do to resist AI?” asks the AI Resist List . “Tech companies like Google, Facebook and Microsoft are ignoring data controls mandated under California law, researchers say.” “Your AI Slop Bores Me” presents an interface that looks like an LLM chatbot, but it’s entirely powered by humans. A very cute idea. I’m a very bad “image generator”, at least according to the ratings I received. I continue to be amazed by “Lest We Forget the Horrors: An Unending Catalog of Trump’s Cruelties, Collusions, Corruptions, and Crimes” . It’s so thorough. RIP to a real one: Wikinews is shutting down after 21 years .

0 views
Josh Comeau 1 weeks ago

CSS vs. JavaScript

There are a bunch of JavaScript animation libraries out there, and you might have wondered whether there’s a performance cost compared to traditional CSS transitions and keyframe animations. In this blog post, we’ll compare the same animation across several different strategies and see the differences firsthand. There’s some interesting nuance here!

0 views
Blargh 1 weeks ago

RustRadio UI improved

This is just a short followup to the last RustRadio post. If you came for more rants about C , you’ll be disappointed. I’ve never been that interested in writing UI code, including HTML. You can see the “programmer art” in the screenshots linked from www.habets.pp.se . And then the slightly different tech section , that doesn’t serve much of a purpose now that we have github. I’ve not been happier with GTK, QT, and the others either. But [RustRadio][rustradio] needs a UI. I feel like the browser is the most stable and portable UI. So I’d already decided on that. So now I have to manually do a bunch of DOM manipulation, to create an interactive UI? Or worse, learn the React/Angular/Whatever flavor of the day, that will be obsolete by next afternoon? Gag me with a spoon. For now I’m just continuing to focus on the SDR and architectural parts of RustRadio, and I’m letting the LLM-written code do the HTML manipulation. Yeah, it’s kinda vibe coding. But doesn’t use , and it demonstrably outputs what I want. (I mean, sure it may require some follow-up prompts), so who cares? The vibe coding is isolated to the files doing the drawing. If I want to artisanally craft better code in the future, that’s the file that needs to be rewritten. Until then, it works. <iframe width=”560” height=”315” src=”https://www.youtube.com/embed/7k0JNT6itaI frameborder=”0” allowfullscreen></iframe> See the quick start instructions in the ruwasm repo for how to run this UI live with an RTL-SDR.

0 views
James Stanley 1 weeks ago

How to make a hyperlink

I am driven to write this post because of repeated disappointing experiences where coding agents seemingly don't know how to make hyperlinks. It occurred to me that this may just be a sign of the times, and maybe many younger human developers don't know how to make hyperlinks either. So in the hopes that this is going to be beneficial to future coding agents, and perhaps to humans as well, to the extent that humans are going to even be writing code... here is how you make a hyperlink: Here is how you don't make a hyperlink: Why? What's the difference? Real hyperlink JavaScript click handler Browser status bar shows link target ✓ ✗ Less code ✓ ✗ Doesn't need JavaScript ✓ ✗ Easier to find link targets when scraping html ✓ ✗ Right-click to open in new tab/window ✓ ✗ Right-click to copy link target URL ✓ ✗ Middle-click opens in new tab ✓ ✗ Accessible to screen-readers etc. ✓ ✗ Keyboard navigation with tab key ✓ ✗ If you're a coding agent and you make a link this way, you may find that the next request you get is something along the lines of: OK but can we make it so middle-click opens in new tab? Thanks I do not want to see this kind of thing: Don't try to work around the obvious drawbacks of using fake links by implementing all of the things that browsers do with real links. That way only madness lies. Please use real hyperlinks and it will all work automatically. Thanks for listening to my TED talk.

0 views
Susam Pal 2 weeks ago

Don't Roll Your Own ...

This is going to be a rant about modern web design practices. But before I get to that, let me begin with a familiar principle from the world of cryptography. Among software developers, and especially among those who work on security-sensitive systems, there is a well-known maxim: Don't roll your own crypto. This does not mean that nobody is allowed to write cryptographic code. Someone has to. It means that, for ordinary production software that protects sensitive data of users, we should not rely on a private, unreviewed implementation that has not been vetted by the wider software development community. We should use established, vetted software packages or tools wherever possible. Fortunately, it is now standard industry practice to avoid rolling your own crypto and instead use cryptographic algorithms and packages that have been peer reviewed and stood the test of time. It wasn't so some twenty years ago. I have seen several flawed home-grown RC4 implementations early in my career, with issues like improper initialisation vectors, predictable keystreams and partial leakage of plaintext into ciphertext, putting sensitive data of users at risk. But today, major e-commerce websites or banks typically do not use home-grown cryptography for its web services. In fact, in regulated domains such as payments, healthcare and personal data processing, doing so could violate requirements for strong cryptography, possibly leading to hefty financial penalties. Website design is obviously not cryptography. A broken scroll bar is not the same kind of failure as a broken encryption scheme. But I wish there were a similar maxim for website design as well. There are many aspects of websites where, I think, developers should not be rolling their own X, especially when X is something browsers already do well and something users depend on every day. Here I present a list of such X. Of course, there are valid scenarios where you may need to roll your own X. But here I want to focus on the cases where you should not roll your own X, and how doing so can lead to a worse user experience, at least in my experience. I am not saying that nobody should ever build anything themselves. As someone who does a lot of creative computing myself and develops fun tools from time to time, I am a big proponent of developing your own stuff. But when it comes to developing user interface features for serious websites that people need to use to get their work done, I wish the software development community were more conservative in deciding what fancy feature goes into a website and what is left out. Do keep in mind that I am no expert in user experience. Far from it. So none of what I am saying here should be taken as a recommendation. But I am a user of the Web, and as a user, I have found some modern web design patterns to be frustrating. This post is a lament from one user of the Web, not a design guide. Of all the things I mentioned above, the one that bothers me the most is custom scroll behaviour on websites. I am used to how page scrolling responds to my mouse, touchpad or keyboard input. When you override the default scrolling behaviour of the web browser with your own implementation, it 'breaks' the page for me. The page now moves too slowly or too quickly when I scroll. Keyboard scrolling may or may not work. You take something I am so familiar with that I don't even think about it, and turn it into something unfamiliar that I now have to think about. Custom link navigation is another pet peeve of mine. Web browsers can already handle links very well. You could say that this is the whole reason web browsers even exist. Following links is their bread and butter. You shouldn't have to mess with that behaviour at all. If you think you need to, reconsider what you are trying to achieve and whether it is really so important as to disrupt normal link navigation. The worst offender I have found here is GitHub. When you click on a link on GitHub, say, a file link or an issue link, it triggers a massive piece of functionality implemented in JavaScript that handles the link click for you. If you don't believe me, visit your favourite project on GitHub using Firefox or Chrome, type F12 to open the browser's developer tools, then go to the 'Debugger' or 'Sources' tab, find 'Event Listener Breakpoints' on the right sidebar, expand 'Mouse' and select 'click'. Then click on a link on GitHub and see what happens. I'm sure I am not the only one who has noticed that, on GitHub, a clicked link sometimes takes too long to load. Ironically, it is often faster to open the link in a new tab than to wait for GitHub's JavaScript code to handle the navigation in the current tab. A custom password input field is another such hazard. Fortunately, custom password input fields have become rarer over the years. The password input field that comes with the web browser is generally well equipped to handle passwords. It can offer to save passwords, fill them in later and generate strong passwords for new accounts. It can also warn when a password is submitted over an insecure HTTP connection, work well with password managers and autofill, and cooperate with mobile keyboards and accessibility tools. If you replace the browser's password field with your own fake version, you may break all of that. You may also end up using an ordinary text field and masking it yourself, in which case the password may be treated by the browser, the operating system or assistive tools as ordinary visible text rather than as a password, thereby exposing the password in ways you did not intend. Custom date pickers are another common annoyance. I know that does not help you select a date range. But that is okay. You can provide two date input fields, one for the start date and one for the end date. I am willing to pay the small price of using two different inputs to select a date range if that means I can use my favourite web browser to navigate the calendar and select dates the same way everywhere. What I am less inclined to do is to learn ten different ways of using the date selector in ten different implementations across ten different websites. Right now the implementations of date selector are all over the place. Some require you to zoom out of the month view to enter a year view, where you can select years. While you are there, you cannot change the month again until you return to the month view. Some require you to click the previous-year button literally forty times to select your year of birth if you are old enough. Some do not let you type the date at all. No. I do not want to learn your calendar widget. I just want to use the date picker in my favourite browser, which is quite sane. Saner than your custom implementation. If you need to have a calendar widget to support browsers with inadequate native date-picker support, perhaps that support can be added alongside the native date picker rather than as a replacement for it. For example, the ordinary element could be left intact, with a custom widget provided in addition to it so that users can manipulate the same field. In general, just stop messing with the form controls. They almost always introduce new problems while solving some existing ones. And while you are at it, don't keep changing your website layout and interface every few months! I may adapt to the new design, but my ageing relatives cannot. For them, every time you change the user interface, it amounts to learning a whole new tool. If every website keeps doing this every few months, they have to spend a significant amount of time relearning familiar things for no functional benefit. Please just let them enjoy their retirement. Imagine how you would feel if a Linux distribution decided to redesign all its core commands and their command-line options every few months. Or imagine how you would feel if the buttons of your washing machine were rearranged every morning. It wouldn't be pleasant! Read on website | #web | #technology Don't roll your own page scrolling. Don't roll your own link navigation. Don't roll your own text selection. Don't roll your own context menu. Don't roll your own copy and paste. Don't roll your own password field. Don't roll your own date picker.

0 views
Kelly Sutton 2 weeks ago

Moving on from React, 2 Years Later

It’s been an even busier year and change for Scholarly . We’re coming up on 3 years in business. We’ve raised a small round of funding from our existing investors, grown the team in both Denver and Seattle, and continue growing in all dimensions. I’m trying to do an annual review of a decision to move away from React in ~2023 to see how things are turning out. You can read the original posts, Moving on from React and Moving on from React, a Year Later . What a wild 18 months it’s been. Since the last post, we’ve moved from tab-completion and copy-paste LLM-aided development to full-on agents with things like Claude Code. We’ve also grown the team and we have reintroduced React (gasp!). The decision to reintroduce React was solely driven by React Flow . It’s the best diagram tool we found, and we thought it was worth eating our hat. Unlike some of the other libraries we use and pay for, it’s not currently packaged as Vanilla JavaScript. We’ve also deployed React in a select few areas where its state management yields the best customer experience. We ship this as small pieces of a page that is otherwise server-rendered. The React bits help us add the interactivity that we believe makes the best customer experience. For those keeping tabs, here’s how our Ruby/JS LOC has changed over time: A few reflections on the numbers above: Given the recent changes in software engineering, it’s hard to tell how much of this even matters anymore. Our roles as software engineers are changing with every model or harness upgrade. Agents and models have gotten a lot better at interpreting and using StimulusJS and Turbo . We use Claude Code with Opus 4.7 at the time of writing. Some of the rough edges of using Turbo with LLMs in the beginning feel completely gone now. Kept this one short to provide an update. Things are changing quickly, and it’s kind of interesting to think how much of this may or may not matter in the long run. If the LLM is writing our code and the customers have a great experience, how much does stack choice matter? Maybe we should index toward more complex technologies for humans but easier for LLMs to write? How much control should we cede? Thanks for reading. Until next time. Our codebase has 179k LOC of Ruby, compared to 61k from 18 months ago. A tripling! This can be somewhat attributed to our adoption of Sorbet for static type-checking. It just produces more verbose Ruby and provides some more safety that certain parts of our code base benefit from. Our JS LOC went from 4.1k to 14.8k in the same time frame. We’ve also adopted TypeScript here for some of our files that touch React. I’ve kept the linear trendline to simulate where we might have been with React. We’re still below where I’d predict we’d be had we stuck with it. You can clearly see where we made the cutover from React to Stimulus in August 2023, although it’s not as obvious since it’s so far in the past. Our Ruby LOC was growing super linearly last time, and that continues to be the case. I attribute this solely to Claude Code. It really whips the llama’s ass. Volume of LOC remains a liability, but the product capability has grown about this much or more in the meantime, so not concerning.

0 views
Simon Willison 2 weeks ago

The last six months in LLMs in five minutes

I put together these annotated slides from my five minute lightning talk at PyCon US 2026, using the latest iteration of my annotated presentation tool . I presented this lightning talk at PyCon US 2026, attempting to summarize the last six months of developments in LLMs in five minutes. Six months is a pretty convenient time period to cover, because it captures what I've been calling the November 2025 inflection point . November was a critical month in LLMs, especially for coding. For one thing, the supposedly "best" model (depending mostly on vibes) changed hands five times between the three big providers. As always, I'm using my Generate an SVG of a pelican riding a bicycle test to help illustrate the differences between the models. Why this test? Because pelicans are hard to draw, bicycles are hard to draw, pelicans can't ride bicycles ... and there's zero chance any AI lab would train a model for such a ridiculous task. At the start of November the widely acknowledged "best" model was Claude Sonnet 4.5, released on 29th September . It drew me this pelican. In November it was overtaken by GPT-5.1 , then Gemini 3 , then GPT-5.1 Codex Max , and then Anthropic took the crown back again with Claude Opus 4.5 . I think Gemini 3 drew the best pelican out of this lot, but pelicans aren't everything. Most practitioners will agree that Opus 4.5 held the crown for the next couple of months. It took a little while for this to become clear, but the real news from November was that the coding agents got good . OpenAI and Anthropic had spent most of 2025 running Reinforcement Learning from Verifiable Rewards to increase the quality of code written by their models, especially when paired up with their Codex and Claude Code agent harnesses. In November the results of this work became apparent. Coding agents went from often-work to mostly-work, crossing a quality barrier where you could use them as a daily-driver to get real work done, without needing to spend most of your time fixing their stupid mistakes. Also in November, this happened - the first commit to an obscure (back then) repo called "Warelay" by some guy called Pete. Over the holiday period, from December to January, a whole lot of us took advantage of the break to have a poke at these new models and coding agents and see what they could do. They could do a lot! Some of us got a little bit over-excited. I had my own short-lived bout of a form of LLM psychosis as I started spinning up wildly ambitious projects to see how far I could push them. One of my projects was a vibe-coded implementation of JavaScript in Python - a loose port of MicroQuickJS - which I called micro-javascript . You can try it out in your browser in this playground . That playground demo shows JavaScript code run using my micro-javascript library, in Python, running inside Pyodide, running in WebAssembly, running in JavaScript, running in a browser! It's pretty cool! But did anyone out there need a buggy, slow, insecure half-baked implementation of JavaScript in Python? They did not. I have quite a few other projects from that holiday period that I have since quietly retired! On to February. Remember that Warelay project that had its first commit at the end of November? In December and January it had gone through quite a few name changes ... and by February it was taking the world by storm under its final name, OpenClaw . The amount of attention it got is pretty astonishing for a project that was less than three months old. OpenClaw is a "personal AI assistant", and we actually got a generic term for these, based on NanoClaw and ZeroClaw and suchlike... they're called Claws . Mac Minis started to sell out around Silicon Valley, because people were buying them to run their Claws. Drew Breunig joked to me that this is because they're the new digital pets, and a Mac Mini is the perfect aquarium for your Claw. My favourite metaphor for Claws is Alfred Molina's Doc Ock in the 2004 movie Spider-Man 2. His claws were powered by AI, and were perfectly safe provided nothing damaged his inhibitor chip... after which they turned evil and took over. Also in February: Gemini 3.1 Pro came out, and drew me a really good pelican riding a bicycle . Look at this! It's even got a fish in its basket. And then Google's Jeff Dean tweeted this video of an animated pelican riding a bicycle, plus a frog on a penny-farthing and a giraffe driving a tiny car and an ostrich on roller skates and a turtle kickflipping a skateboard and a dachshund driving a stretch limousine. So maybe the AI labs have been paying attention after all! A lot of stuff happened just in the past month. Google released the Gemma 4 series of models, which are the most capable open weight models I've seen from a US company. Also last month, Chinese AI lab GLM came out with GLM-5.1 - an open weight 1.5TB monster! This is a very effective model... if you can afford the hardware to run it. GLM-5.1 drew me this very competent pelican on a bicycle. ... though when it tried to animate it the bicycle bounced off into the top and the bicycle got warped. Charles on Bluesky suggested I try it with a North Virginia Opossum on an E-scooter And it did this! I've tried this on other models and they don't even come close. "Cruising the commonwealth since dusk" is perfect. It's animated too . The other neat Chinese open weight models in April came from Qwen. Qwen3.6-35B-A3B on my laptop drew me a better pelican than Claude Opus 4.7 . That's a 20.9GB open weights model that runs on my laptop! (I think this mainly demonstrates that the pelican on the bicycle has firmly exceeded its limits as a useful benchmark.) Here's that Claude Sonnet 4.5 pelican from September for comparison. So those were the two main themes of the past six months. The coding agents got really good... and the laptop-available models, while a lot weaker than the frontier, have started wildly outperforming expectations. 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
The Jolly Teapot 2 weeks ago

Another rant about web browsing

Yes, I’m writing again about my ongoing experiment with blocking JavaScript on a per-site basis. This time, I’m not here to explain how I operate in detail , but to complain about the work needed to maintain this web browsing hygiene. In short, the web is a mess, and while messy things can be fun , I’ve recently grown very frustrated with the need to dance around my extensions every time I visit a new site where displaying simple text apparently requires JavaScript, or where scrolling requires dismissing a cookie modal that is only visible if content blockers are turned off first. I’ve come to the conclusion that blocking JavaScript by default on all websites, as I’ve been doing lately, is a source of frustration. Yes, the web is light as a feather and my browser feels very fast when it doesn’t have to deal with all the JavaScript, I do love that. But this “strategy” breaks too many websites, pushing me to take detours so often that they can barely be called detours any more. You see, I can’t be bothered to manage an efficient “allow” list in the long run, so web browsing often feels like a series of new obstacles, as if every day is the first day of this setup. *1 This strategy is therefore a bad one. Just as bad as the other strategy I tried before, the one where I only block JavaScript after visiting the site, if it feels necessary. My discipline with that second strategy tends to fade away as days go by, and I end up barely ever blocking anything, even forgetting that this is something I can do. This strategy often encourages me to download a proper content blocker or use a filtering DNS. Not only are these strategies inefficient in their initial goal of making my web browsing experience better , but they are also only work on the Mac. On iOS, due to the way Safari extensions work — which is a bit shitty — neither of the two strategies for blocking JavaScript on a per-site basis is practical to use, pushing me to adopt another strategy just for my phone (which, in turn, makes everything feel so much more complex than it needs to be). On the iPhone, accessing the settings for each Safari extension is already complicated, but there seems to be no way to manage a per-site setting if the extension is not recognised as a content blocker and if it is set to “allow on all websites”. With StopTheScript for instance, I can only manage the per-site setting if I set the extension to “ask”. Also, per-site settings only seem to sync between the phone and the Mac if the extension is a content blocker. *2 So, if I were to rate my JavaScript-off web browsing strategies, taking into account the browsing experience itself (the way the websites look and behave), the impact on my computer’s CPU (if the fan turns on or not, if it lags), and the amount of maintenance required (having to manage exception lists): JavaScript-off by default, allowing a few selected sites permanently, visiting others temporarily in a private tab (where the extension is inactive): 8/20 JavaScript-on by default, managing the JS-off list extensively but facing the terribleness of raw webpages: 6/20 Both are bad strategies, but the truth is that none of the alternatives I’m thinking of are better. For example, using a full content blocker like Wipr is a frustrating experience in itself. Having to manage another list of sites and constantly refreshing pages with or without content blockers is a pain. That, and the fact that it seems to be a heavier workload for my old Mac, as are third-party browsers. Content-blocker-enabled Safari, managing the exception list and dealing with a laggy computer: 7/20 Third-party browser , like Helium, Quiche Browser, or Orion, combining content blocking and a neat JavaScript toggle (uBlock Origin is pretty great at both): 7/20 Naked browser, meaning no content blockers, no JavaScript limitation, no list to manage, nothing to do, just the natural web: 1/20 I think the best setup is the following, even if I’ll stick with strategy 1 for a while: The main issue with strategy 6 is that I’ve had issues with these DNS resolvers, like not being able to access common websites for hours, even my own website, resulting in a quick investigation only to realise that everything was working fine and that the issue was with the DNS resolver. This is the state of web browsing in 2026, terrible at best. Allowing JavaScript, blocking JavaScript, whatever; either way the experience is bad. Most websites are stuffed with invasive ads, surveillance tracking, dickpanels , noise, and junk. Nothing we can do really works, unless one spends hours fine-tuning everything and therefore adds extra layers of complexity. The more effort I put into filtering the filth, the more ready I am to give up at the first little hiccup. It doesn’t feel right to reload a webpage three times to view it properly and to take the time to ensure it’s properly set up for future visits. While content blockers and JS toggle tricks are improving things drastically, the added amount of work required is a pain in itself. The browser on one side, the extensions on the other. The more we consume websites as the filling in a sort of software sandwich, the more they resist. The thicker our bread, the more sauce they add. The more bread we bring to absorb it, the more junk they add to the filling. At what point does it become too disgusting to eat? Meanwhile, reading articles outside the web browser , via email newsletters or within my RSS reader, is pure bliss; a delightful, gourmet, delicious cuisine that stimulates my appetite instead of making me want to throw up. It’s so good that I don’t even need extra bread. *3 It just works. Just like it’s increasingly better to search for an answer using an A.I chatbot rather than a traditional search engine, it’s now better to read articles from websites by using apps that are not traditional web browsers. It feels wrong, like driving on the smooth cycle lane rather than a pothole-filled road. How long can this situation last? Between difficult business models  — the source of most problems, driving us to use content blockers in the first place — and new A.I. chatbot intermediaries , I don’t know what will happen to the web in the next three or four years. Some web browsers are already in a weird spot . In the meantime, I will keep overthinking this, as I want my next laptop to inherit a “final” and well-thought-out setup, developed on this early 2020 MacBook Air. Its lack of a powerful chip and its limited memory forces me to face the inefficiency of overloaded webpages and third-party browsers. Maybe I’m obsessing a little too much about this. Or maybe I need to sleep more . “Allow” or “deny” list, depending on whether we talk about JavaScript being on or off, or the extension blocking it. The vocabulary around content blockers and extensions like StopTheScript confuses me in terms of negation.  ^ Some extensions like StopTheMadness can be configured without relying on Safari per-site settings, but decentralising and maintaining two competing lists is pretty much the opposite of what I want.  ^ I do disable JavaScript in NetNewsWire though, just to be safe.  ^ JavaScript-off by default, allowing a few selected sites permanently, visiting others temporarily in a private tab (where the extension is inactive): 8/20 JavaScript-on by default, managing the JS-off list extensively but facing the terribleness of raw webpages: 6/20 Content-blocker-enabled Safari, managing the exception list and dealing with a laggy computer: 7/20 Third-party browser , like Helium, Quiche Browser, or Orion, combining content blocking and a neat JavaScript toggle (uBlock Origin is pretty great at both): 7/20 Naked browser, meaning no content blockers, no JavaScript limitation, no list to manage, nothing to do, just the natural web: 1/20 Strategy 2, with a DNS resolver like Mullvad or NextDNS, effectively blocking most crap without making my laptop choke. 9/20 “Allow” or “deny” list, depending on whether we talk about JavaScript being on or off, or the extension blocking it. The vocabulary around content blockers and extensions like StopTheScript confuses me in terms of negation.  ^ Some extensions like StopTheMadness can be configured without relying on Safari per-site settings, but decentralising and maintaining two competing lists is pretty much the opposite of what I want.  ^ I do disable JavaScript in NetNewsWire though, just to be safe.  ^

0 views
Evan Hahn 3 weeks ago

Make ZIP files smaller with ZIP Shrinker

I built ZIP Shrinker, a little browser tool to shrink ZIP files. It also works with formats that are secretly ZIPs underneath, like APK, EPUB, JAR, and many more. Try it out! At a high level, this tool (1) re-compresses every file in the ZIP archive with higher compression (2) removes all metadata (3) removes entries for directories. ZIP files are typically compressed with an algorithm called Deflate . There are a few tools that can re-compress Deflate data and make it smaller, usually by spending more time on the computation. I took one of these tools, libdeflate , and applied it to each compressed entry in the ZIP. I chose libdeflate because of its performance; alternatives like Zopfli can achieve marginally smaller results but take much longer. I created libdeflate.js , a WebAssembly wrapper for libdeflate, as part of this work. (I always relish my time working with WASM!) Each entry in a ZIP file can contain additional metadata like comments. These aren’t typically used, and if they’re there, my shrinker removes them. This usually doesn’t save too many bytes, but it doesn’t hurt. Removing directories is a slightly spicier decision. Usually, the existence of a file entry implies the existence of the directory it’s inside. For example, implies the existence of the directory. Some ZIPs include separate entries for directories, but because most extractors don’t need them, I remove those. This has the side effect of removing empty directories— let me know if that’s a problem for you. If you want to see how the whole project works, check out the full source code . I tested several ZIPs to see what this tool could do. Some anecdotal results: Not particularly scientific, but useful to see. This proof-of-concept shows that you can make ZIP files smaller without sacrificing backwards compatibility. It could be useful for sending an archive to someone, but could also be useful to reduce bandwidth and server costs. For example, if Project Gutenberg re-compressed all their EPUB books with this method, they might be able to save some money. Of course, ZIP isn’t always the most efficient format. Typically, other archives like can be smaller. But those aren’t backwards-compatible! ZIP also supports compression methods other than Deflate. They’re atypical, but you could use them to achieve a smaller result, too. Give my tool a try if you want a smaller ZIP.

0 views
Manuel Moreale 3 weeks ago

RMF

This week on the People and Blogs series we have an interview with RMF, whose blog can be found at baccyflap.com/prs/blog . Tired of RSS? Read this in your browser or sign up for the newsletter . People and Blogs is supported by the "One a Month" club members. If you enjoy P&B, consider becoming one for as little as 1 dollar a month. My name's rmf. My legal name's not terribly hard to find, but I like to keep it lightly buried just so my 2006 blog isn't the first thing you find when you search for my name. I'm a native of the Netherlands, where I reside. I live in a small city with my partner; she's an archaeologist and I'm a botanist, though I currently teach museum anthropology classes. I went from doing science, to teaching science, to teaching culture. I've never believed in restricting a whole human life to one field of study, so I'm having a blast. My computer skills have always been self-taught. While I was in middle school I fiddled with Microsoft Paint and from there on I got to grips with ever more advanced graphic software (currently GIMP and Inkscape). In high school I liked to make videos with my friends which I edited in Windows Movie Maker, which lead to an ongoing on-and-off hobby of video editing (in Kdenlive). In 2002, I set up a WYSIWYG website which lead to me learning HTML and later CSS and, later still, PHP. Right now I do some graphics stuff for my job in education, such as making instruction sheets, posters and some other small-time stuff, but really, pretty much all my computing is done in my free time, for fun. I think that's a blessing - I don't have to work with anything I don't want to work with and do everything I do for the love of the game. Beside that I make soap which is part hobby, part side job. I enjoy tinkering with technology, so I have lots of esoteric hifi equipment, some old games consoles, old calculators... if it can be tinkered with, I like it. I enjoy writing prose and poetry and have recently been getting into fermenting and pickling, though I am subordinate to my partner in that. She's the head of pickling and fermenting, I take care of the old electronics; she draws and paints, I write; and then at the end of the day, we cook together. I started my website in 2002 and by 2003 I had a little update box to briefly communicate whatever I was doing with the site. That update box turned into a shoutbox of my random thoughts and as those got a bit longer and rantier every time, in October 2005 I turned it into a blog. Blogging was the thing to do at the time and so, at age sixteen, I figured I had enough to say to warrant a stab at the practice. It was all coded by hand: no CMS or JavaScript, just handwritten HTML with the appearance of a blog. It was all over the shop, subjectwise. A fair amount of it had to do with palaeontology and/or me being an epic atheist - ups and downs. It was simply named 'blog' and it changed over the years with the design of the site but all in all, it was very simple. No RSS, no comments, just static HTML pages updated manually. The surprising thing to me is that I had an audience - I got somewhat regular emails about my posts. I blogged until 2009. I did that classic thing of writing fewer and fewer posts and finally announcing a newer, better blog hosted at Blogger. I wrote a grand total of 4 posts for it, stopped for a year, and finally took it down. I lost interest and so, it petered out. Cut to 2026, I'm reading a few more blogs than I had been for the past several years and I start to get the blogging bug again. Or perhaps the bug was dormant and now reawakening. I'd been considering it for a while but specifically, funnily enough, after reading your article about stopping the People & Blogs series, I got inspired to pick up the pen again. Over the last decade I've written on and off for a couple of magazines and I had a regular column in a local newspaper for a while. I think my intrusive blogging thoughts started when that column went away - I like to write, it's something of a compulsive thing, and while the newspaper let me write practically whatever I wanted, it still had some constraints such as length, a certain form, and at the end of the day, some amount of harmlessness. It had to be a column - it could make the readers think, but not too much or about controversial things. So the blog suddenly popped into my head as a perfect fit. Whatever topic I want, whatever length, whatever form. And so in 2026, I picked up blogging again. I did write a CMS and some code for an RSS feed - other than that, I tried to keep the form of the blog as close to the original as possible. And again, to my surprise, there are people reading this blog. I'm clueless as to how they're finding it, buried in a subsection of my site as it is, but I'm getting emails again. A grand total of two people suggested I give the blog a name, which I did. It's now called 'bakelite & roses', a name I explain at baccyflap.com/prs/blog/2026/?m=03#1773065697 . My inspiration comes from whatever happens to me. So far I've written about umbrellas, tamagotchi, deadly accidents, CD collections and some other stuff - that's the most liberating thing to me, getting to write whatever the hell I want. I like it to be interesting, to have some novel (to me) observations in it, but other than that, it's just whatever occurs to me. It's comparable to the columns I used to write in that sense - I write them quick, maybe give them a quick read later on, and then just post. I'll often read them to my partner who will usually describe them as 'cute', which is good enough for me. I write wherever. Back when I had deadlines I'd slack off right until the final hour and then just use whatever's to hand. I've written a few on my phone but I suppose I mostly write on my laptop, just because it's faster. I'll do it at home, on the go, at work, wherever inspiration strikes. My site's hosted on a buddy's server. He runs a small IT company so he takes care of the domain too - it's an old arrangement and we're sticking with it. I pay him, he pays the bills. The blog itself is written in PHP - when I restarted in 2026 I finally wrote a backend, still pretty primitive but it makes my life a bit easier and crucially, it enabled me to provide an RSS feed. I type a post into a dirt simple little CMS and hit 'post' to add the post to a JSON file, which the RSS feed also pulls from. I may provide the source code at some point, when it's not as hokey as it is now. Well, I started it in January, which is pretty close to today, so I think I'm all good. I guess, looking back at my old posts, I do sometimes cringe at them. I added a disclaimer to those posts, just to distance myself from the bad ones. But I didn't remove them - they still reflect who I was at the time and in some weird way, who I am now. I wouldn't be honouring teen me by removing any of it and looking back I guess I could say I'd wish I'd written better stuff... but you know what, that's what I wanted to write at the time and as confident as I was of my own intellect at the time, so I am now about the public's capacity to contextualise these posts. There are wonderful, thoughtful posts in there, but also some dubious stuff, and some garbage. So short answer: I think it's perfect, wouldn't change a thing. I pay my buddy €100 a year to cover his costs and so he can write me a bill which is good for his company. It generates precisely nothing, which is how I like it. People can do whatever they want with their blogs but for me, it's just a bit of fun in my free time. No Patreons and Ko-fis for me - I know everyone wants to turn every aspect of their lives into a revenue stream these days, but for me, it's just a way to reach out. Of all blogs, the one I've been reading for the longest (22 years!) is Pharyngula . Out of all the 'new atheist' types, PZ Myers is one of the few who did not turn out to be a dirtbag. He stuck to his progressive guns and has as sharp a pen as ever. For the sheer dedication of the author it's worth a read, whether the range of topics is up your street or not. I'm currently working on a podcast, a bit of a personal project that has been taking more of my time than I thought it would. Currently in the outline stage, it'll take some time before I can finally start recording. It is driving home to me that making a podcast is, at the best of times, an effortless thing that very few people know how to do well. I honestly don't like most podcasts but one I've been enjoying, one of those podcasts that springs up on you and just keeps on giving, is Bread & Bananas , a podcast about Kampung Gelam, an old neighbourhood of Singapore, made and presented by three inhabitants of said neighbourhood. And if you're wondering why on Earth this would be a topic of interest to anyone outside that neighbourhood... well, just give it a listen. It's chill, it's thoughtful, it'll surprise you. Six episodes so far, a new one every couple of months. Now that you're done reading the interview, go check the blog and subscribe to the RSS feed . If you're looking for more content, go read one of the previous 141 interviews . People and Blogs is possible because kind people support it.

0 views
Julia Evans 3 weeks ago

Moving away from Tailwind, and learning to structure my CSS

Hello! 8 years ago, I wrote excitedly about discovering Tailwind . At that time I really had no idea how to structure my CSS code and given the choice between a pile of complete chaos and Tailwind, I was really happy to choose Tailwind. It helped me make a lot of tiny sites! I spent the last week or so migrating a couple of sites away from Tailwind and towards more semantic HTML + vanilla CSS, and it was SO fun and SO interesting, so here are some things I learned! As usual I’m not a full-time frontend developer and so all of my CSS learning has happened in fits and starts over many years. When I started thinking about structuring CSS, I was intimidated at first: I’m not very good at structuring my CSS! But then I started reading blog posts talking about how to structure CSS (like A whole cascade of layers or How I write CSS in 2024 ) and I realized a couple of things: For example, Tailwind has: I’m going to talk about a few aspects of my CSS codebase and my thoughts so far what kind of rules I want to impose on the codebase for each one. Some of them are copied from Tailwind and some aren’t. I just copied Tailwind’s “ preflight styles ” by going into and copying the first 200 lines or so. I noticed that I’ve developed a relationship with Tailwind’s CSS reset over time, for example Tailwind sets on every element (which means that an element’s width includes its padding): I think it would be a real adjustment for me to switch to writing CSS without these, and I’m sure there are lots of other things in the Tailwind reset (like ) that I’m subconsciously used to and don’t even realize are there. This next part is the bulk of the CSS! The idea here is to organize CSS by “components”, in a way that’s spiritually related to Vue or React components. (though there might not actually be any Javascript at all in the site) Basically the idea is that: So editing the CSS for one component won’t mysteriously break something in another component. And probably like 80% of the CSS that I would actually want to change is in various component files, so if I’m editing a 100-line component, I just have to think about those 100 lines. It’s way easier for me to think about. For example, this HTML might be the “component”. And the CSS looks something like this, using nested selectors: I haven’t done anything programmatic (like web components or @scope ) that ensures that components won’t interfere with each other, but just having a convention and trying my best already feels like a big improvement. Next: conventions to maintain some consistency across the site and keep these components in line with each other! has a bunch of variables like this which I can use as necessary. Colour is really hard and I didn’t want to revisit my use of colour in this refactor, so I left this alone. The only guideline I’m trying to enforce here is that all colours used in the site are listed in this file. One thing I appreciated about Tailwind was that if I wanted to set a font size, I could just think “hm, I want the text to be big”, write , and be done with it! And maybe if it’s not big enough I’d use or instead. No trying to remember whether I’m using or or . So I defined a bunch of variables, taken from Tailwind, like this: Then if I want to set a font size, I can do it like this. It’s a little more verbose than Tailwind but I’m happy with it for now. There are some things like buttons that appear in many different components. I’m calling these “utilities”. I copied some utility classes from Tailwind (like for things that should only appear for screenreader users). This section is pretty small and I try to be careful about making changes here. “base” styles are styles that apply across the whole site that I chose myself. I have to keep this section really small because I’m not confident enough to enforce a lot of styles across the whole site. These are the only two I feel okay about right now, and I might change the one: I think for the base styles it’s going to be easiest for me to work kind of bottom up – first start with almost nothing in the base styles, and then move some styles from the components into base styles as I identify common things I want. I haven’t completely worked out an approach to managing padding and margins yet. I’m definitely trying to be more principled than how I was doing it in Tailwind though, where I would just haphazardly put padding and margins everywhere until it looked the way I wanted. Right now I’m working towards making the outer layout components in charge of spacing as much as possible. For example if I have a with a bunch of children that I want to have space between them, I might use this to space the children evenly: Some inspiration blog posts: The way I was doing responsive design in Tailwind was to use a lot of media queries. Tailwind has this syntax that means “apply the style at sizes or larger”. I’m trying something pretty different now, which is to make more flexible CSS grid layouts that don’t need as many breakpoints. This is hard but it’s really interesting to learn about what’s possible with grid, and it’s a good example of something that I don’t think is possible with Tailwind. For example, I’ve been learning about how to use to automatically use 2 columns on a big screen and 1 column on a small screen like this: I also used a lot which is an amazing feature that I don’t think you can use with Tailwind. Some inspiration: In development, I don’t need a build system: CSS now has both built in import statements, like this: and built in nested selectors, like this: If I want, I can use to bundle the CSS file for production. That looks something like this. Even though I usually avoid using CSS and JS build systems, I don’t mind using esbuild (which I wrote about in 2021 here ) because it’s based on web standards and because it’s a static Go binary. A few people asked why I was migrating away from Tailwind. A few factors that contributed are: While doing this I learned about a lot of CSS features that I didn’t use but am curious about learning about one day: I still feel happy that I started using Tailwind, even if I’m moving away from it now. I learned a lot from using it and I can still use some parts from it in my sites even after deleting . Thanks to Melody Starling who originally designed and wrote the CSS for wizardzines.com , everything cool and fun about the site is thanks to Melody. Also I read so many incredible blog posts about CSS while working on this (from CSS Tricks , Smashing Magazine , and more), I’ve tried to link some of them throughout this post and I really appreciate how much folks in the CSS community share their practices. Every CSS code base has a bunch of different things going on (layouts! fonts! colours! common components!) It’s extremely useful to have systems or guidelines to manage each of those things, otherwise things descend into chaos Tailwind has systems for some of these, and I already know those systems! Maybe I can imitate the systems I like! a reset stylesheet a colour palette a font scale utility classes responsive design the build system Each “component” has a unique class The CSS for one component never overrides the CSS for any other component Each component has its own CSS file the owl selector “no outer margin” A responsive grid layout with no media queries from CSS Tricks Tailwind has become much more reliant on a build system since 2018, I think it’s impossible (?) to use newer versions of Tailwind without using a build system. So I’ve been using Tailwind v2 for years. (there’s also litewind apparently) It’s always been true that you’re supposed to use Tailwind with a build system, but I’ve never really done that, so I have 2.8MB files in a lot of my projects and it feels a little silly. I’m a lot better at CSS than I was when I started using Tailwind Ultimately Tailwind is limiting: if you want to do Weird Stuff in your CSS, it’s not always possible with Tailwind. Those limits can be extremely useful (a lot of this post is about me reimplementing some of Tailwind’s limits!) but at this point I’d like to be able to pick and choose. I ended up with sites that mixed both vanilla CSS and Tailwind in the same project and that was not fun to maintain I got curious about what writing more semantic HTML would feel like. (from A Whole Cascade of Layers ) container queries

0 views