Hey Claude, Generate an Interactive Quiz

A little family game we have going right now is asking one of the many AI products available to generate a quiz on a random topic.

CoPilot generates a nice interactive quiz with the prompt:

Generate for me an interactive intermediate quiz with 10 questions, and a topic of Dubai landmarks.

CoPilot Quiz
CoPilot Quiz

Claude, though, always goes one step further. I gave Claude the following prompt:

Generate for me an extremely hard interactive quiz of 20 technical questions on the topic of Really Simple Syndication (RSS).

I got an entire interactive site. Good luck.1

Claude Quiz
Claude Quiz

  1. Embarassingly, I did shit. 10 out of 20.

Hello, Astro

The Gobbler blog is powered by Astro. This website used to be powered by Ghost. But I like consistency, so this website is now powered by Astro as well.

With the help of Claude, all posts have been moved over while maintaining their original guids for RSS readers, all images downloaded from Ghost, all historical WordPress and Ghost HTML stripped, and all code blocks have been correctly indented 1.

This move means membership and newsletters have been retired. Everything that was previously gated by membership barriers is now available to all. For anyone concerned, Ghost will delete all data around the start of April. So sad.

Tangentially related to all this website work, I have discovered something new 2 (at least new to me): Universally Unique Lexicographically Sortable Identifiers (or ULIDs). They are kind of like UUIDs but sortable and decodable. In previous static sites where posts have had front matter, I’ve always used the post slug or a random UUID as a post identifer for feeds, but ULIDs seem to serve this purpose better. This post, for example, has a unique ID of 01KKC42KAQ8SWA4FTXVGWKJCKE and you can decode that at ulid.page.3 (On the new Gobbler blog I am using nanoIDs as post identifiers. It’s nice to have options!)

For those that are interested, I create a new post by running npm run new "Blog Title", which runs this node scripts/new-post.mjs, which leads to this:

#!/usr/bin/env node
/**
 * Creates a new post with a ULID postId.
 */

import { writeFile } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { ulid } from 'ulid';

const __dirname = dirname(fileURLToPath(import.meta.url));
const POSTS_DIR = join(__dirname, '../src/content/posts');

const title = process.argv[2];
if (!title) {
  console.error('Usage: node scripts/new-post.mjs "Post Title"');
  process.exit(1);
}

const slug = title
  .toLowerCase()
  .replace(/[^\w\s-]/g, '')
  .replace(/\s+/g, '-')
  .replace(/-+/g, '-')
  .trim();

const id = ulid();
const now = new Date();
const date = now.toISOString();
const datePrefix = now.toISOString().slice(0, 10);
const filePath = join(POSTS_DIR, `${datePrefix}-${slug}.md`);

const content = `---
title: "${title}"
postId: "${id}"
date: ${date}
tags: []
excerpt: ""
---

`;

await writeFile(filePath, content);
console.log(`Created: src/content/posts/${datePrefix}-${slug}.md`);
console.log(`postId:  ${id}`);

Much easier than manually crafting front matter.


  1. This took about an hour.

  2. via Jon Sterling’s blog.

  3. Nice! And, if you can’t tell, I’m excited to have footnotes working again.

Featured

Rolling.bike

Rolling.bike

A useful new web app for generating ride briefs is now available at the brilliantly named rolling.bike. Upload your GPX file, or sync your routes via Strava or Ride with GPS, provide some context and Rolling will generate a well structured ride brief.

Here’s a walked through example:

The GPX file
The GPX file

A quick summary of this route:

  • Start at Botanics
  • The hilly comb of First-to-Fifth Avenue
  • Hillview
  • Lorong Sesuai
  • Rifle Range Road
  • Finish at Dempsey at the bottom of the map

Information I provided to Rolling:

  • Route Name: Hills and Thrills
  • Category: Steady (2.0 - 2.8 W/kg)
  • Named Start and End points
  • Rider Notes:
  • New Rider Instructions: Lots of short and snappy hills.
  • Self sufficiency note: Two bottles. One gel.

With that information, Rolling can output various lengths of brief: Regular, Event, or WhatsApp, depending on your selection. In addition, you can pick your brief’s tone (I use cheeky). With the above information, the Regular output for this ride:

Hills and Thrills is exactly what it says on the tin, a fun steady-paced loop round some of the nicer parts of the island. It’s 35.2 kilometres with 635 metres of climbing spread across six proper little kicks, so you’ll get a proper workout but nothing that’ll have you gasping for air round every corner. This ride is for anyone who fancies a morning out with decent scenery and a few punchy climbs to keep things interesting. We’ve got regroup points dotted through the route, so you can catch your breath and stay together as a group. No heroics required. The climbs are all short and snappy. Haunted Hill at 14.7 kilometres in is probably the meanest of the lot at 8.7 per cent, but it’s still only 300 metres of pain. Lorong Sesuai later on is similarly spicy at 11 per cent but equally brief. First Avenue and Bukit Timah Road will warm you up early doors, nothing outrageous but enough to let you know your legs are switched on. You’ll be self-sufficient out there, so bring two bottles and a gel to keep the tank topped up. A spare tube and pump are non-negotiable, as is the usual common sense. We’re starting at Singapore Botanic Gardens Tanglin Gate on Saturday 7 March at 6am. It’s an early one but worth it for the cooler morning air and quiet roads. We finish at Burnt Ends Bar on Dempsey Road, where we’ll sort out proper breakfast and a coffee that’s actually worth drinking. Should be a cracking morning.

The Event brief:

This is a steady ride built around short, punchy climbs that’ll keep things interesting without requiring you to sell a kidney. We’re looking at 35 kilometres with 635 metres of elevation crammed in, which is basically Singapore’s way of testing whether you can handle variety on a bike. We’ll start at Tanglin Gate in the Botanic Gardens and head into familiar territory. The climbs here are properly bite-sized. You’ve got Jalan Dermawan, which is steep enough to remind you that you have legs, and Lorong Sesuai sits in there with a bit of attitude at 11 per cent average. Holland Road rounds out the main event near the end. Nothing’s long enough to really damage you, but they’re all short enough that you have to actually pedal properly. This isn’t a race, so we’ll regroup sensibly at the bottom of anything notable. Keep things social on the flats, push on the climbs, and try not to look too smug when you get to the top. We’re aiming for around two hours ten minutes of riding, which gives us time to chat, recover between efforts, and generally not take ourselves too seriously. The route loops us back through some nice sections, so there’s rhythm to it. Early on you’ll pass through Avenues One to Five before heading out toward the real climbing. Bukit Timah Road gets involved early, then we settle into the longer middle section where Jalan Dermawan and Lorong Sesuai are waiting to have their say. Bring two bottles and a gel minimum. There aren’t convenient shops along the way, so come prepared to look after yourself. Water stops are on you. We’re finishing at Burnt Ends Bar on Dempsey Road, which is a proper reward for a solid morning’s work. Coffee, food, legs that feel like they’ve done something worth doing. That’s the deal. See you Sunday morning. And no, you don’t need to be elite to ride this. Just show up with a bike that works and legs that are willing to go up hills a few times.

And finally, the WhatsApp brief:

Saturday 7 March, 6am sharp from Tanglin Gate. We’re doing Hills and Thrills, 35 km of the sort of climbing that keeps you honest. Plenty of short punchy hills that’ll test your legs but won’t bore you. Regroup spots at First through Fifth Avenue at the bottom, then Former Hillview and Lorong Sesuai at the top, plus Rifle Range Road at the end. Bring two bottles and a gel because we’re not messing about. Finish at Burnt Ends Bar, Dempsey Road.

The more you use Rolling the better it gets as it actually learns your tone!

It’s an extremely useful utility, and a bargain at $15 AUD a year.

Gobbler — Early Access

I’m thinking about releasing a new web-based RSS reader and RSS aggregator called Gobbler. It’s available to test for the next month or so and you can sign up using this link. Everything is still in sandbox, so you won’t be charged for Stripe. All data will be removed at the end of March.

If you want to use Gobbler on mobile:

  • Reeder: Use Google Reader
  • NetNewsWire: Use FreshRSS
  • Current: Use FreshRSS
  • *or *just add it to your Home Screen from Safari

Gobbler on Desktop

Gobbler on iPhone

You can find out more about Gobbler at gobbler.press.

If you have feedback—I’d be delighted to hear it—please send to support@gobbler.press.

What does “Updated Database” actually mean?

Every month or so I update Singapore Buses with the latest data from the Land Transport Authority. And every month the *What’s New *section on the App Store usually says something along the lines of:

Updated database and routes.

What does that actually mean? Well, starting with 2026.3, which is Waiting for Review, I’ve decided to include the month-to-month differences in the *What’s New *section. For this release, that means:

New Stops (1)

CodeDescriptionRoad
74951Blk 962ATampines St 96

Location Changes (7)

CodeDescriptionDistance
14521Village Hotel320m
28221Opp Intl Business Pk53m
28239Jurong Town Hall Int48m
28301Blk 13121m
43081Opp St. Joseph’s Ch (Bt Timah)112m
59069Opp Blk 75774m
67759Compassvale Int13m

Name Changes (4)

CodeOld NameNew Name
47749W’lands Health CampusW’lands Hosp
52241Blk 105Bef Caldecott Stn/SAVH
65091BLK 301ABlk 301A
67759COMPASSVALE INTCompassvale Int

Service Changes (20)

CodeDescriptionAddedRemoved
74949Blk 961A18M
74959Blk 969C18M
74961Blk 96618M
74969Opp Blk 96618M
75341Opp The Clearwater Condo18M
75349The Clearwater Condo18M
84201Bedok Resvr Stn Exit A18M
84209Bedok Resvr Stn Exit B18M
84221Blk 10918M
84229Blk 11118M
84251Blk 9918M
84259Blk 8818M
84261St. Anthony’s Cano Sch18M
84269Blk 9518M
84271Aft Bedok Ind Pk E18M
84279Aft Bedok Nth St 518M
84521Blk 50618M
84529Blk 10918M
84591SBST Bedok Nth Depot18M
84599Opp SBST Bedok Nth Depot18M

The biggest surprise to me was the number of bus stops moving around and how far they move. I mean *Village Hotel *is almost half a kilometre away from where it was before — that’d be a shock in the morning if it was where you started your commute.

Matrix for NetNewsWire

0:00
/0:02

Matrix

This theme is *Matrix-*inspired and was built with the help of Claude (there’s simply no way my CSS is this clean and tidy). This theme shines when your device is in dark mode.

Features

Visual Style

  • Phosphor-green palette — deep near-black background with glowing green text
  • CRT vignette — a radial gradient overlay darkens the screen edges for a vintage monitor feel (macOS only)
  • Boot flicker — the page and article animate in with a CRT power-on flicker effect
  • Monospace throughout — SF Mono

Article Header

  • Feed name is prefixed with a shell prompt: user@nnw:~$ cat
  • Publish date is prefixed with # timestamp:
  • External link is prefixed with # source:
  • Feed icon is rendered in greyscale with a green tint and a pixel-art rendering mode

Article Title

  • Typewriter animation — the title types out character by character in reading order, correctly handling titles that span multiple lines
  • Blinking block cursor () appears after the last character lands and blinks indefinitely

Article Body

  • Separator line of characters between the header and body
  • Headings prefixed with Markdown-style ##, ###, #### markers
  • Blockquotes styled with a left border and faint green background
  • Code blocks include a fake terminal title bar (● ● ● output) and horizontal scroll on overflow
  • Inline code highlighted in amber with a subtle glow
  • Tables use uppercase headers, row hover highlights, and a green glow on the border
  • Figcaptions prefixed with //
  • Images are desaturated and dimmed; hovering partially restores colour

Platform Behaviour

  • iOS — uses dynamic type sizing, system hyphenation, and respects safe area insets; html background is set so native navigation and tab bar blur effects sample the correct dark colour
  • macOS — includes the CRT vignette, wider padding, and fixed text-size classes (smallTextxxlargeText)

Add to NetNewsWire

Featured

Experiments with the Codex and Claude Agents

Experiments with the Codex and Claude Agents

I’ve been experimenting with agents: Codex on a Python-to-ExpressJS conversion, and Claude in a Swift Package. The results have been impressive.

The remainder of this post was originally for members only.

Codex

Singapore Buses has a back-end server, written in Python with FastAPI, that is used to manage storage of APNS tokens, retrieve bus arrival estimates and push that data out as Live Activity notifications, and storage of aggregated session events for users. I wrote the server application in Python simply to get some experience with the language. I have, however, been wanting to re-write it as an ExpressJS app for quite some time.

Enter Codex.

I downloaded Codex and gave it permission to the project’s directory. I gave it one prompt:

Can you convert this project to ExpressJS?

Codex took less than two minutes to convert public routes, middleware, database, and schedulers, while selecting and replacing third-party Python packages with similar npm packages (e.g., apns2 was replaced with @parse/node-apn).

The results was a working application with one defect—the payload for Live Activities was empty 🫣. Once that was fixed and the rest of the code tested, it was more-or-less a drop in replacement on the server (after changing PM2’s configuration file).

Claude

In Xcode, I enabled Claude’s agent and gave it dominion over my Swift Package LandTransportKit which is what I use in Singapore Buses. The goal of this experiment was to have it write documentation for the package as a Documentation Archive (DocC). This is something I’ve been meaning to do for quite some time, as I wrote when I published the package in July 2025:

I used the ChatGPT integration to write the documentation for most of struct , class, and func definitions. It hasn’t been used for the code itself, the test cases, or DocC.

The prompt:

Write documentation for this package in the documentation archive.

The results speak for themselves. Not only is the package extensively documented, there’s also example usage in UIKit and SwiftUI, along with prebuilt SwiftUI Views.


These experiments have left me impressed with what the Codex and Claude agents are capable of. Both were fast, understood the prompts, and, ultimately, I got the results I wanted.

NetNewsWire 7 for iOS Out Now

NetNewsWire 7 for iOS Out Now

After five-ish years of NetNewsWire 6—and many Summer, Autumn, and Winter nights of coding through 2025 and into 2026, and a sizeable TestFlight window—The World’s Favourite Open Source RSS ReaderTM has reached version 7. (Note: we discovered and fixed a lot of bugs during TestFlight, so thanks go to the testers!)

Similar to the Mac release, NetNewsWire 7 for iPhone and iPad:

  • requires the OSs 26
  • adopts Liquid Glass
  • is a significant under-the-hood overhaul that adopts Swift Concurrency

But, but, but…unlike the Mac release, which was quite easy, the iOS release has required a lot more work. On the latest episode of *The Talk Show, *Brent rightly pointed out that “iOS apps are just more complicated”.

To summarise my Design Diaries and some additional items, NetNewsWire 7 makes 30 major changes:

  • [Sidebar] Converted from UITableView to UICollectionView. This was needed in order to adopt modern styling across iPad and iPhone. iPad uses the .sidebar style, and iPhone uses .insetGrouped. This is similar to the behaviour you see in Mail.
  • [Sidebar] The current *Refresh *status is now located in the navigation bar as a subtitle, having previously been the footer.
  • [Sidebar (iPad)] Like the Mac refresh, the Feeds view floats and allows Timeline content to slide underneath.
  • [Sidebar] Smart Feeds and Account headers now adopt modern secondary styling.
  • [Sidebar (iPad)] Selected feeds have a modern capsule background and the text is bold.
  • [Sidebar] Folders have been redesigned to match modern standards—they now have the same indentation as any other feed, but the enclosed feeds are indented further.
  • [Sidebar] Folders will highlight when Feeds are being dragged and dropped into them.
  • [Sidebar] Separators have been realigned.
  • [Sidebar] Unread counts are larger and are no longer backed by a filled capsule.
  • [Sidebar] Unread counts for folders are only displayed when the folder is closed.
  • [Sidebar] Swipe actions reveal icons.
  • [Sidebar (iPad)] Users can resize the sidebar (within reason).
  • [Timeline] Converted from UITableView to UICollectionView (during TestFlight builds!) This was needed in order to adopt modern cell styling—e.g., selected and swipe status—across iPad and iPhone.
  • [Timeline] Now uses UICollectionViewDiffableDataSource.
  • [Timeline] Navigation bar images have been removed.
  • [Timeline] Unread counts are now located in the navigation bar subtitle.
  • [Timeline] Adopts hierarchical text colours for titles and summaries.
  • [Timeline (iPad)] The search bar has been moved to the app-wide toolbar and behaves similar to search on the Mac.
  • [Timeline (iPhone)] The search bar has been moved to the bottom toolbar.
  • [Timeline (iPad)] The Timeline width is user adjustable (again, within reason).
  • [Timeline] Timeline cells have been redesigned in Interface builder and now have the rounded corner selection style in addition to hierarchical text colours for title and summary.
  • [Timeline] The Mark All as Read image (on both iPad and iPhone) has had alignment changes to make sure it sits in the middle of an englassified button.
  • [Article (iPad)] Articles can be read in three-pane view without hiding the Sidebar.
  • [Article (iPad)] The top toolbar inherits search capabilities.
  • [Article] The bottom toolbar buttons have been grouped in a 2-1-2 formation with the *Next Unread *button sitting in the throne seat.
  • [Sidebar, Timeline, Article] Visual state is restored on relaunch.
  • [Widgets] Home Screen widgets have been redesigned to make better use of horizontal space.
  • [Widgets] Added a new Lock Screen widget with Today, Unread, Starred counts.
  • [Settings] Timeline Customiser has been redesigned and includes both icon and non-icon previews.
  • [About] Tending to the dark corner of the garden, the About view on iOS has been redesigned and inspired by the Credits from Vesper.

The new About view.

NetNewsWire 7 for Mac Out Now

NetNewsWire 7 for Mac Out Now

NetNewsWire 7 for Mac is out now.

This release is significant: it adopts Swift Concurrency (which you shouldn’t see), Liquid Glass (which, despite the transparency, you should see), and supports macOS 26 (as a minimum). From a UI perspective:

  • The Sidebar adopts standard Liquid Glass behaviours which means it floats and allows timeline content to slide underneath
  • Unread indicators are no longer backed by a filled capsule. They are now just a simple unread count
  • The Toolbar has seen a minor reorganisation which moves the sidebar toggle from the timeline into the sidebar. In addition, toolbar buttons adopt the standard Liquid Glass button look-and-feel
  • Tending to the dark corner of the Lucida Grande garden, the About window has been redesigned

Nick Heer, Pixel Envy:

This is a rather tasteful implementation of Apple’s new visual design language

The iOS release is just around the corner.

A Triple-Negative Pretzel

I could not fail to disagree with you less. — Boris Johnson

My interpretation of this ridiculous sentence:

  • “fail to disagree”* *equates to “agree”, therefore, with some substitution:
  • “I could not agree with you less” suggests you’re at the maximum end of the disagreement scale.

Regardless, I asked both ChatGPT, Claude, and Gemini what the *plain English *meaning of this statement was and got two completely opposing answers:

  • ChatGPT described the sentence as a *triple-negative pretzel, *with the plain English meaning: I completely disagree with you.
  • Claude and Gemini both stated the opposite: I totally agree with you.

So what does it mean?