/quack

The colophon

You found the page that keeps score. Below: what is hidden on this site and how much of it you have turned up, and then an honest account of how the site works — the parts that are more interesting than they look, and the parts that are less. Eleven of the twelve are hidden somewhere. The twelfth is not hidden at all — it stays locked until the other eleven are found, and it is worth the walk.

Easter eggs

0 of 12 found

  • hiddenDevelopers look in places visitors do not.
  • hiddenUp, up, and you already know the rest.
  • hiddenThe duck likes attention. A lot of it.
  • hiddenDucks are only half the obsession. Type the other half.
  • hiddenAsk a duck what a duck says. Spell it out.
  • hiddenOne word, one route.
  • hiddenRobots get a file. So do people.
  • hiddenGo somewhere that does not exist.
  • hiddenEvery duck in every park knows one word. Type it.
  • hiddenCome back at an hour you should not be awake at.
  • hiddenProgrammers have been explaining their code to ducks for decades.
  • lockedNot hidden anywhere. This one unlocks when the other 11 are found — 11 to go.

Robots get a file on every website, by an old convention nobody enforces. People should too, so there is a humans.txt here as well.

How this is built

Rendering

Next.js 15 with the App Router, React 19, TypeScript in strict mode. MUI v7 with Emotion for the interface and framer-motion for anything that moves.

Everything you can navigate to is HTML generated at build time — the 64 project write-ups, the section indexes, this page. Exactly one route runs per request: /api/ducky, which is marked dynamic = 'force-dynamic' because it has to reach a language model. Nothing else on the site touches a server after it is deployed.

Content

The facts — projects, roles, awards, education, skills — are hand-written JSON in src/data. Each file has a zod schema, and the root layout calls assertContentValid() before it renders anything, so a typo in a data file fails the build instead of shipping a broken page.

The long-form write-ups are Markdown in src/content/projects, one file per project, split on ## headings when anything needs to read them a section at a time.

The admin that is not here

There is a small CMS for editing all of that — collection lists, forms, image uploads. You cannot reach it, and neither can anyone else, because every one of its files is named *.dev.tsx and pageExtensions in next.config.ts only includes that extension outside a production build.

That is a stronger guarantee than a password: Next builds its route matcher directly from pageExtensions, so in production those pages and their write endpoints are not protected — they do not exist as routes at all. A runtime guard sits behind them anyway, on the principle that one layer is a plan and two is a decision.

Ducky, the duck that answers questions

Retrieval is BM25 — plain lexical search, k₁ 1.5, b 0.75 — over 334 passages built at module load from the same JSON and Markdown you can read on the rest of the site. No vector store, no embedding API, no second network hop.

That is a choice, not a shortcut. This corpus is almost entirely proper nouns: library names, company names, project names, award names. Those are exactly the queries an exact term match wins and a semantic one fumbles. A literal bonus lifts a chunk whose title or slug appears verbatim in your question, so typing a project name finds it even with no other shared vocabulary.

The language model only writes the prose. Citations are chosen by the retriever and resolved against what it actually returned, which means the duck cannot link you to a project it did not read.

The duck itself

One requestAnimationFrame loop, reading refs. Everything continuous — travel, footprint spacing, the idle clock — lives in that loop, and React state changes only when the duck decides something: a handful of times a minute rather than sixty times a second.

Time comes from the rAF clock rather than Date.now(), so a duck left in a background tab does not come back convinced it slept for twenty minutes. Under prefers-reduced-motion it stops wandering entirely and parks in the corner, where it remains a perfectly good button.

With thanks to the people who maintain Next.js, MUI and framer-motion, to whoever decided in 2011 that websites should credit the humans who make them, and to the duck, who has no idea any of this is about it.

← back to the site