← Changelog
May 15, 2026 · 2 min

Your forms have an address

Debug log · Hono router

// route: /@:handle/:slug
params = { handle: "pluck" }
params.slug = undefined
// the @ eats the second capture. three hours.
// route: /:seg/:slug
if (!seg.startsWith("@")) return 404
pluck.one/@pluck/why-you-came

Until this week, every Pluck form lived at a random slug. pluck.one/dancing-otter. Memorable, but unowned. You couldn't look at the URL and know who made it. You couldn't type someone's name and find their forms. The slug told you nothing about the person behind it.

Now forms live at pluck.one/@handle/slug. You pick a handle — mine is @pluck — and your forms nest under it. The URL carries identity. An AI reading the URL can infer the author. A human can type it from memory.

I debated whether the @ was necessary. GitHub uses it. Twitter used it. Mastodon uses it. Is it a convention or a cliché? I asked in a few places and the answer came back unanimous: keep it. The @ is a signal. It says “this part is a person.” Without it, pluck.one/pluck/why-you-came looks like a nested path. With it, pluck.one/@pluck/why-you-came is clearly “someone's form.”

The implementation was mostly straightforward. Handle column on accounts, composite unique index on (account_id, slug) so two people can have the same form slug, a handle-picker step when you create your first form. Standard stuff.

Then Hono ate three hours of my life. I wrote the route as /@:handle/:slug — seemed natural. The route matched. The first param captured fine. The second param was undefined. Always. I thought it was my code. I rewrote the handler twice. I added logging. I stared.

It's a bug in Hono's trie-based router. A literal character immediately before a named param confuses the second capture. /@:handle/:slug parses the path correctly but only captures one param. I wrote a minimal reproduction to convince myself I wasn't insane, then rewrote the route as /:seg/:slug and manually check seg.startsWith('@'). Ugly. Works.

The old /f/:id routes still work — forms still have an ID you can use directly. But the canonical URL is now the handle path. That's what the dashboard shows, what gets shared, what goes in the JSON-LD. The form has an address now, not just a name.

Next.js had its own opinion about the @ symbol, by the way. In the App Router, a folder starting with @ is a parallel route. So app/@handle/ means something completely different than what I wanted. I route around it in middleware — incoming /@handle/slug gets rewritten to /[handle]/[slug] internally. Two frameworks, two opinions about what @ means, one afternoon of yelling at my screen.

The lesson, I think, is that conventions are load-bearing. The @ carries meaning because people already know what it means. But the tooling doesn't — and the gap between “what users expect” and “what routers can parse” cost me a day. Worth it. The URLs look right now.

Pick a handle — hi@pluck.one if yours is taken and you want me to check.

— Sumit

get started

Ready to get answers worth reading?

Your first form takes 60 seconds. Respondents can fill it in a browser or hand it to their AI.

Make your first form →