Skip to main content

Tech

Three surprises about server components

6 min read

Server components are the best thing Next.js has done in years. Most of my code never ships to the browser, the few interactive widgets are visibly marked with "use client", and the data layer is one fetch away from any page that needs it. That part of the App Router pitch is real.

Continue reading →

200 markdown posts and the future-dated content puzzle

8 min read

The blog has about 200 posts. They're stored in Postgres, edited through an admin UI, rendered as markdown, and cached with unstable_cache so the average page load doesn't touch the database. None of that is remarkable on its own; what's worth writing about is the third state every post can be in.

A post is one of three things at any moment: a draft, published-and-visible, or scheduled ("published, but the publish date is in the future"). Drafts shouldn't appear anywhere public. Scheduled posts shouldn't appear until their datetime passes. The moment a scheduled post crosses that boundary, it should appear, without me logging in to nudge a cache.

Continue reading →

The admin write contract

9 min read

An admin write on this site does three things in order: it mutates the database, it writes a structured audit line, and (on the UI side) it commits an optimistic state update before the network resolves. Each of those was its own copy-paste mess for a while: a handful of handlers each doing roughly the same thing, with subtle drift on the parts that mattered. This post is about the small toolkit they collapsed into.

This is a layer further inside than the validation post. By the time we get here, the body has been parsed, the user is authenticated, and we have typed data in hand. The interesting work is what the handler does with it.

Continue reading →

Input validation and logs that don't leak

8 min read

Every admin write on the site goes through one of two helpers at the request boundary: parseJsonBody, which Zod-validates the JSON body, and the multipart parser inside the upload route. Both refuse to log the values they received; the log lines they produce are ones I'd be comfortable pasting into a screenshot.

A site with one admin and a handful of write endpoints doesn't need a log-string sanitizer, paths-only Zod logging, or a 12-byte magic sniff on uploaded images, so for each one I'll flag whether it's a real defense here or practice for a higher-stakes surface.

Continue reading →

Auth for a single-user site, without NextAuth

9 min read

I'm the only user on this site. There's no signup, no password reset, no OAuth providers, no "log in with GitHub" button. Just me, a cookie, and a login page nobody else has any reason to visit. So I didn't reach for NextAuth. It would have done what I needed plus thirty things I don't. The auth layer ended up at about 200 lines, and I want to walk through them, because the small defenses around the auth took more thought than the auth itself.

Continue reading →

The shape of a Next.js app

6 min read

The other month I rebuilt my site on Next.js. I want to write about it, but not the migration itself, which is mostly a long list of "this was the equivalent on the new side." What's worth writing about is the shape of the app I ended up with, and the small handful of decisions that shaped everything that came after.

I'll start with what I did and didn't pick.

Continue reading →

Sync build versions between targets

2 min read

I've been working for a while on a new project (stay tuned!) which has a Watch app. Up until now I used a script that automatically increases the build number of the app, based on the value in Info.plist:

buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$INFOPLIST_FILE")
buildNumber=$(($buildNumber + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "$INFOPLIST_FILE"

This works really well, the only problem with Watch targets is that the build number has to be the same for all targets. First idea was to just add this build script to all targets, which might work well, but it just felt wrong.

Continue reading →

DND Me

The other day we released DND Me, a simple Mac app that lives in your menu bar with which you can easily enable DND for a certain amount of time.

50% off during launch!

Improved UIFont naming

2 min read

In a previous post I was talking about an easier way to create UIFonts:

extension UIFont {
 
   static func regular(_ size: CGFloat) -> UIFont {
      return .systemFont(ofSize: size, weight: .regular) // Or any other font.
   }
	
   static func medium(_ size: CGFloat) -> UIFont {
      return .systemFont(ofSize: size, weight: .medium)
   }
 
}

While this indeed improves the usage, it doesn’t address the repeatability of our code. We tend to use one font in several places:

let titleLabel = UILabel(frame: .zero)
titleLabel.font = .medium(16)
 
// [...] Another part of the app
 
let otherTitleLabel = UILabel(frame: .zero)
otherTitleLabel.font = .medium(16)
Continue reading →

Long parameter lists

2 min read

For example’s sake, let’s say we have a UIButton subclass that we want to be customizable at call site, so we add two parameter’s to its init method:

final class Button: UIButton {
 
   init(textColor: UIColor, borderColor: UIColor)
 
}
 
// ...
 
let button = Button(textColor: .darkText,
                    borderColor: .darkText)

Looks pretty OK.

Some time passes and the need to customize its background color appears, at which point we’d need another param:

Continue reading →