More from Applied Cartography
When we added support for complex filtering in Buttondown, I spent a long time trying to come up with a schema for filters that felt sufficiently ergonomic and future-proof. I had a few constraints, all of which were reasonable: It needed to be JSON-serializable, and trivially parsable by both the front-end and back-end. It needed to be arbitrarily extendible across a number of domains (you could filter subscribers, but also you might want to filter emails or other models.) It needed to be able to handle both and and or logic (folks tagged foo and bar as well as folded tagged foo or bar). It needed to handle nested logic (folks tagged foo and folks tagged bar or baz.) The solution I landed upon is not, I’m sure, a novel one, but googling “recursive filter schema” was unsuccessful and I am really happy with the result so here it is in case you need something like this: @dataclass class FilterGroup: filters: list[Filter] groups: list[FilterGroup] predicate: "and" | "or" @dataclass class Filter: field: str operator: "less_than" | "greater_than" | "equals" | "not_equals" | "contains" | "not_contains" value: str And there you have it. Simple, easily serializable/type-safe, can handle everything you throw at it. For example, a filter for all folks younger than 18 or older than 60 and retired: FilterGroup( predicate="or", filters=[ Field( field="age", operator="less_than", value="18" ) ], groups=[ FilterGroup( predicate="and", filters=[ Field( field="age", operator="greater_than", value="60" ), Field( field="status", operator="equals", value="retired" ) ] groups=[], ) ] )
If there's been one through line in changes to Buttondown's architecture over the past six months or so, it's been the removal and consolidation of dependencies: on the front-end, back-end, and in paid services. I built our own very spartan version of Metabase, Notion, and Storybook; we vended a half-dozen or so Django packages that were not worth the overhead of pulling from PyPI (and rewrote another half-dozen or so, which we will open-source in due time); we ripped out c3, our visualization library, and built our own; we ripped out vuedraggable and a headlessui and a slew more of otherwise-underwhelming frontend packages in favor of purpose-built (faster, smaller, less-flexible) versions. [1] There are a few reasons for this: Both Buttondown as an application and I as a developer have now been around long enough to be scarred by big ecosystem changes. Python has gone through both the 2.x to 3.x transition and, more recently, the untyped to typed transition; Vue has gone from 2.x to 3.x. The academic problem of "what happens if this language completely changes?" is no longer academic, and packages that we installed back in 2018 slowly succumbed to bitrot. It's more obvious to me now than a few years ago that pulling in dependencies incurs a non-trivial learning cost for folks paratrooping into the codebase. A wrapper library around fetch might be marginally easier to invoke once you get used to it, but it's a meaningful bump in the learning curve to adapt to it for the first time. It is easier than ever to build 60% of a tool, which is problematic in many respects but useful if you know exactly which 60% you care about. (Internal tools like Storybook or Metabase are great examples of this. It was a fun and trivial exercise to get Claude to build a tool that did everything I wanted Metabase to do, and save me $120/mo in the process.) We still use a lot of very heavy, very complex stuff that we're very happy with. Our editor sits on top of tiptap (and therefore ProseMirror); we use marked and turndown liberally, because they're fast and robust. On the Python side, our number of non-infrastructural packages is smaller but still meaningful (beautifulsoup, for instance, and django-allauth / django-anymail which are both worth their weight in gold). But the bar for pulling in a small dependency is much higher than it was, say, twelve months ago. My current white whale is to finally get rid of axios. 39 call sites to go! ↩︎
After many wonderful years of working out of my home office (see Workspaces), I've now "expanded" [1] into an office of my own. 406 W Franklin St #201 is now the Richmond-area headquarters of Buttondown. Send me gifts! The move is a bittersweet one; it was a great joy to be so close to Haley and Lucy (and, of course, Telly), and the flexibility of being able to hop off a call and then take the dog for a walk or hold Lucy for a while was very, very nice. At the same time, for the first time in my life that flexibility has become a little bit of a burden! It turns out it is very hard to concentrate on responding to emails when your alternative is to play with your daughter giggling in the adjoining room; similarly, as Buttondown grows and as more and more of my time is spent on calls, it turns out long-winded demos and onboarding calls are logistically trickier when it is Nap Time a scant six feet away. And, beyond that, it's felt harder and harder to turn my brain off for the day: when there is always more work to be done, it's hard not to poke away at a stubborn pull request or jot down some strategy notes instead of being more present for my family (or even for myself, in a non-work capacity.) So, I leased an office. The space is pretty cool: it's downtown in the sweet spot of a little more than a mile away from the house: trivially walkable (or bikeable, as the above photo suggests) but far enough away to give me a good bit of mental space. The building is an old manor (turned dormitory, turned office building). I've got a bay window with plenty of light but no views; I've got a nice ethernet connection and a Mac Mini with very few things installed; I've got a big Ikea desk and a printer; I've got an alarm on my phone for 4:50pm, informing me that it's time to go home, where my world becomes once again lively and lovely, full of noise and joy and laughter. Air quote because I'm fairly confident this office is actually smaller than the home office. ↩︎
Buttondown's core application is a Django app, and a fairly long-lived one at that — it was, until recently, sporting around seven hundred migration files (five hundred of which were in emails, the "main" module of the app). An engineer pointed out that the majority of our five minute backend test suite was spent not even running the tests but just setting up the database and running all of these migrations in parallel. I had been procrastinating squashing migrations for a while; the last time I did so was around two years ago, when I was being careful to the point of agony by using the official squash tooling offered by Django. Django's official squashing mechanism is clever, but tends to fall down when you have cross-module dependencies, and I lost an entire afternoon to trying to massage things into a workable state. This time, I went with a different tactic: just delete the damn things and start over. (This is something that is inconsiderate if you have lots of folks working on the codebase or you're letting folks self-host the codebase; neither of these apply to us.) rm rf **/migrations/* worked well for speeding up the test suite, but it was insufficient for actually handling things in production. For this, I borrowed a snippet from django-zero-migrations (a library around essentially the same concept): from django.core.management import call_command from django.db.migrations.recorder import MigrationRecorder MigrationRecorder.Migration.objects.all().delete() call_command("migrate", fake=True) And voila. No fuss, no downtime. Deployments are faster; CI is much faster; the codebase is 24K lines lighter. There was no second shoe. If you were like me 24 hours ago, trying to find some vague permission from a stranger to do this the janky way: consider the permission granted. Just take a snapshot of your database beforehand just in case, and rimraf away.
More in technology
Guinness is one of those beers (specifically, a stout) that people take seriously and the Guinness brand has taken full advantage of that in their marketing. They even sell a glass designed specifically for enjoying their flagship creation, which has led to a trend that the company surely appreciates: “splitting the G.” But that’s difficult […] The post This Arduino device helps ‘split the G’ on a pint of Guinness appeared first on Arduino Blog.
AI is everywhere, but most websites are still managed manually by humans using content management systems like WordPress and Drupal. These systems provide means for tagging and categorizing content. But over time, these structures degrade. Without vigilance and maintenance, taxonomies become less useful and relevant over time. Users struggle to find stuff. Ambiguity creeps in. Search results become incomplete and unreliable. And as terms proliferate, the team struggles to maintain the site, making things worse. The site stops working as well as it could. Sales, engagement, and trust suffer. And the problem only gets worse over time. Eventually, the team embarks on a redesign. But hitting the reset button only fixes things for a while. Entropy is the nature of things. Systems tend toward disorder unless we invest in keeping them organized. But it’s hard: small teams have other priorities. They’re under pressure to publish quickly. Turnover is high. Not ideal conditions for consistent tagging. Many content teams don’t have governance processes for taxonomies. Folks create new terms on the fly, often without checking whether similar ones exist. But even when teams have the structures and processes needed to do it right, content and taxonomies themselves change over time as the org’s needs and contexts evolve. The result is taxonomy drift, the gradual misalignment of the system’s structures and content. It’s a classic “boiled frog” situation: since it happens slowly, teams don’t usually recognize it until symptoms emerge. By then, the problem is harder and more expensive to fix. Avoiding taxonomy drift calls for constant attention and manual tweaking, which can be overwhelming for resource-strapped teams. But there’s good news on the horizon: this is exactly the kind of gradual, large-scale, boring challenge where AIs can shine. I’ve worked on IA redesigns for content-heavy websites and have seen the effects of taxonomy drift firsthand. Often, one person is responsible for keeping the website organized, and they’re overwhelmed. After a redesign, they face three challenges: Implementing the new taxonomy on the older corpus. Learning to use the new taxonomy in their workflows. Adapting and evolving the taxonomy so it remains useful and consistent over time. AI is well-suited to tackling these challenges. LLMs excel at pattern matching and categorizing existing text at scale. Unlike humans, AIs don’t get overwhelmed or bored when categorizing thousands of items over and over again. And with predefined taxonomies, they’re not as prone to hallucinations. I’ve been experimenting with using AI to solve taxonomy drift, and the results are promising. I’m building a product to tackle this issue, and looking implement the approach in real-world scenarios. If you or someone you know is struggling to keep a content-heavy website organized, please get in touch.
A simple question that takes some effort to answer in a satisfying way.
Tim Hardwick reporting on Gurman’s reporting in Bloomberg, which I don’t have access to, so I’m quoting the MacRumors article: While specific details are scarce, it's supposedly the biggest update to iOS since iOS 7, and the biggest update to macOS since