[
  {
    "slug": "2026-06-12-pg-tamagotchi",
    "title": "pg_tamagotchi: a pet for your Postgres database",
    "date": "2026-06-12",
    "started": "2026-06-12",
    "updated": "2026-06-12",
    "excerpt": "A tamagotchi that lives in your database as a C extension. You hatch it, feed it, clean up its mess, and its mood tracks the health of Postgres itself.",
    "type": "longform",
    "tags": [
      "postgres",
      "c",
      "extensions",
      "projects"
    ],
    "readingTime": 10,
    "wordCount": 1922,
    "highlight": false,
    "image": "/media/pg-tamagotchi-hero.png"
  },
  {
    "slug": "2026-03-21-human-readable-ids",
    "title": "IDs Are a User Interface",
    "date": "2026-03-21",
    "started": "2026-03-21",
    "updated": "2026-03-21",
    "excerpt": "You're debugging a production issue at 11pm, tailing logs, and you see this:\n\n```\nError processing entity 7b3e4d2a-8f1c-4a5b-9e6d-3c2f1a0b8e7d\n```\n\nWhich entity is that? A user? A payment? An order? Now compare:\n\n```\nError processing entity cus_9R2f1a0b8e7d\n```\n\nInstantly: it's a customer. You know which table, which service, which dashboard. Stripe has been doing this since ~2012: `cus_` for customers, `pi_` for payment intents, `ch_` for charges, `sk_live_` and `sk_test_` for keys.\n\nWhether you intend it or not, IDs become a user interface. They get pasted into Slack threads, dropped into support tickets, spoken aloud on incident calls, and shared across teams who have no idea what your database schema looks like. Internal users especially end up treating IDs as handles for things in your system. It's worth putting some thought into their design.\n\n[...287 more words]",
    "type": "longform",
    "tags": [
      "engineering",
      "api-design"
    ],
    "readingTime": 3,
    "wordCount": 420,
    "highlight": false
  },
  {
    "slug": "2025-09-05-speed-up-rust-build-times-mac",
    "title": "Speeding up Rust builds on macOS",
    "date": "2025-09-05",
    "started": "2025-09-05",
    "updated": "2026-02-18",
    "excerpt": "Via [Nicholas Nethercote](https://nnethercote.github.io/2025/09/04/faster-rust-builds-on-mac.html):\n\nOn macOS, **[XProtect](https://support.apple.com/en-gb/guide/security/sec469d47bd8/web)** (part of Gatekeeper) scans every newly created executable for malware. These scans can slow Rust builds down a lot.\n\n> \\[mac\\]OS scans every executable for malware on the first run. This makes sense for executables downloaded from the internet. It's arguably less sensible for executables you compiled yourself.\n\nAccording to the [nexte.st docs](https://nexte.st/docs/installation/macos/#gatekeeper), you can disable these scans for your terminal apps:\n\n- You can stop XProtect scans on your terminal (and other terminal emulators - Ghostty etc) by running `sudo spctl developer-mode enable-terminal`.\n\n[...80 more words]",
    "type": "til",
    "tags": [
      "rust"
    ],
    "readingTime": 1,
    "wordCount": 170,
    "highlight": false
  },
  {
    "slug": "2025-08-31-black-hole-rust",
    "title": "Simulating a black hole in Rust",
    "date": "2025-08-31",
    "started": "2025-08-31",
    "updated": "2026-03-22",
    "excerpt": "[Max Bo](https://maxbo.me) has this really awesome [Observable notebook](https://observablehq.com/@mjbo/plotting-a-black-hole) that\nplots a black hole from Bjorge Meulemeester's [Luminet](https://github.com/bgmeulem/Luminet) data.\n\nIt was based on Jean-Pierre Luminet's 1979 paper [*Image of a spherical black hole with thin accretion disk*](https://articles.adsabs.harvard.edu/pdf/1979A%26A....75..228L), where he plotted this image *by hand(?!)*:\n\n<img src=\"/media/blackhole.jpg\" alt=\"Image of a spherical black hole with thin accretion disk\"\n  className=\"large\" />\n\n[Source](https://arxiv.org/pdf/1902.11196)\n\nI want to learn about WASM and Rust and this seems like a great project to replicate to learn about those things.\nI have limited Rust experience and no WebAssembly experience.\nHopefully I learn something about black holes too, which I currently know nothing about.\n\n[...2246 more words]",
    "type": "longform",
    "tags": [
      "rust",
      "simulation",
      "wasm",
      "learnlog"
    ],
    "readingTime": 12,
    "wordCount": 2349,
    "highlight": false,
    "image": "/media/blackhole.jpg"
  },
  {
    "slug": "2025-08-31-bun-shell",
    "title": "Bun has a built-in shell with template literal syntax",
    "date": "2025-08-31",
    "started": "2025-08-31",
    "updated": "2026-02-01",
    "excerpt": "Just discovered that Bun has a [built-in shell](https://bun.com/docs/runtime/shell) with the most intuitive syntax I've seen:\n\n```javascript\nimport { $ } from \"bun\";\n\nconst response = await fetch(\"https://hill.xyz\");\n\n// Use Response as stdin.\nawait $`cat < ${response} | wc -c`; // 1256\n```\n\nNo more spawning child processes or dealing with callbacks. It even handles pipes:\n\n```javascript\nawait $`cat file.txt | grep pattern | wc -l`.text();\n```\n\nVery cool!",
    "type": "til",
    "tags": [
      "bun",
      "devtools"
    ],
    "readingTime": 1,
    "wordCount": 68,
    "highlight": false
  },
  {
    "slug": "2024-01-22-recipe-cleaning",
    "title": "recipe.cleaning",
    "date": "2024-01-22",
    "started": "2024-01-22",
    "updated": "2026-04-24",
    "excerpt": "<img src=\"/media/recipe.png\" alt=\"recipe.cleaning screenshot\" className=\"large\" />\n\nTired of scrolling past eight paragraphs about someone's dead grandmother to get to the banana bread recipe?\n\nI'm sick of scrolling through SEO slop just to get to the actual recipe.\n\nI wrote a tiny Go service, [recipe.cleaning](https://recipe.cleaning), to skip all of that. Prepend `recipe.cleaning/` to any recipe URL and you get the recipe, nothing else.\n\n```\nhttps://recipe.cleaning/https://www.recipetineats.com/banana-bread-recipe/\n```\n\nTitle, ingredients, instructions. Glorious plain text. Source is at [github.com/hill/recipe-cleaning](https://github.com/hill/recipe-cleaning).\n\n\n\nThe trick is that basically every recipe site already embeds its recipe as structured data so Google can render rich results in search. The data I want is already sitting in the HTML, I just have to pull it out.\n\n[...247 more words]",
    "type": "longform",
    "tags": [
      "web",
      "project",
      "go",
      "url-app"
    ],
    "readingTime": 2,
    "wordCount": 362,
    "highlight": false,
    "image": "/media/recipe.png"
  }
]