Surf the DreamJustin Avery

Colophon

How this site works.

A personal site is a good excuse to try the things I’d hesitate to put in front of a client first. Here is what’s under this one, and why.

The stack

For eighteen years this was a WordPress blog on a server I looked after myself. It now runs onEmDash, an open-source CMS built on Astro, deployed as a single Cloudflare Worker. Content lives in D1 (SQLite at the edge), images inR2, and every page is rendered on request and then cached.

The move was scripted: WordPress exported every post, page and attachment, and EmDash’s importer converted the HTML to Portable Text, a structured JSON format for rich text. Every post kept its old address, so nothing anyone linked to in 2012 should 404. The link-log posts got a proper field for the page they point at, and old IFTTT short links were followed to where they really go.

There’s no server to patch and no PHP to update. Publishing still happens in a real editor, at/_emdash/admin, signed in with a passkey.

Clef, and type-safe AI

Most AI writes text. Clef, a model Cloudflare released in October 2026, doesn’t write anything. You give it some state and a set oftyped questions, and it gives back a probability for every allowed answer. It follows the same “System One” API as TypeSafe’s Jev model, and runs on Workers AI, a binding away from this site.

There are three kinds of question:

  • noul: yes or no, answered as the probability of yes.
  • choice: pick from options you define, with a probability for each.
  • score: rate against an ordered rubric, with a probability per level.

Because the model can only ever return one of your options, its output can’t be malformed and can’t invent a blog post that doesn’t exist. This site’s little client carries that guarantee into TypeScript, so the answer type is inferred from the question:

const answer = await decide({
  state: { visitor_says: "my Ethereum node keeps running out of disk" },
  questions: {
    thread: {
      type: "choice",
      instructions: "Which part of the archive fits best?",
      criteria: {
        infra: "Servers, Docker, NGINX, nodes",
        web3: "Ethereum, NFTs, on-chain art",
        frontend: "CSS, JavaScript, responsive design",
      },
    },
    covered: { type: "noul", instructions: "Does the archive cover this?" },
  },
});

answer.answers.thread.choice;        // "infra" | "web3" | "frontend"
answer.answers.thread.probabilities; // { infra: 0.94, web3: 0.05, frontend: 0.01 }
answer.answers.covered.noul;         // 0.81

The trick is a conditional type and a const generic, so option keys stay literal:

type AnswerFor<Q> =
  Q extends ChoiceQuestion<infer K> ? ChoiceAnswer<K>   // K = your option keys
  : Q extends ScoreQuestion<infer L> ? ScoreAnswer<L>
  : Q extends NoulQuestion ? NoulAnswer                 // probability of "yes"
  : never;

export async function decide<const Qs extends Record<string, Question>>(input: {
  state: unknown;
  questions: Qs;
}): Promise<{ answers: { [Id in keyof Qs]: AnswerFor<Qs[Id]> } }>

The reading compass

The compass on the home page chains two decisions around a search:

  1. Clef routes your words to one of seven threads in the archive, while FTS5 finds keyword matches.
  2. Recent posts from the winning thread join the matches, up to 28 candidates.
  3. Clef picks which candidate you should read first, a probability for each, and separately rates how well the archive covers your question at all.

The probabilities are drawn rather than printed: each one sets the weight of the type on Newsreader’s variable wght axis, from 200 for unlikely to 800 for certain. The whole thing reads a couple of thousand tokens, takes well under a second, and costs a fraction of a penny, because Clef charges nothing for output.

Reading compass

Not sure where to start?
Tell me what you’re into.

Your words go to Clef, a Cloudflare model that doesn’t write anything. It returns a probability for every option it was given. Those numbers set the weight of the type below.

  1. Front-end craft—
  2. Web3 & on-chain—
  3. Servers & DevOps—
  4. Making things—
  5. WordPress—
  6. Life, cricket & surf—
  7. The link log—

What’s next: Typesense

All search goes through one small function, searchPosts(), so the engine behind it can change without the pages noticing. Next up is Typesense, an open-source search engine that keeps its index in memory and is built for search-as-you-type:

  • Typo tolerance. “resposive” still finds responsive design.
  • Facets. Filter by year, category or link-versus-post, with live counts.
  • Synonyms and ranking rules. Teach it that RWD means responsive web design.
  • Hybrid search. Keyword and vector results blended in one query.

EmDash fires a hook whenever content is saved or deleted, which is exactly where a Typesense index gets kept up to date. When it lands, I’ll write up how it works.

    Type to search titles and full text. ↵ opens the full results page.