Projects
In progress/Jan 2025 — present/Design and engineering/3 min read

Personal Website Project

web developmentAIfull-stack
Summary

This site — a research portfolio with a grounded AI assistant, a local search index, and a motion system built to stay out of the reader's way

Stack
Next.jsTypeScriptTailwind CSSContentlayerGSAP

You are looking at it. The source is on GitHub.

An academic site has one job: let someone work out what you do, decide whether it is any good, and get in touch. Most of the decisions here follow from that, and a few of them came from getting it wrong first.

Content as files

Publications and projects are MDX files in the repository. Contentlayer types them at build time, so a missing venue or a malformed date fails the build instead of rendering an empty span in production. Adding a paper means adding a file.

Everything is statically generated. There is no database, no CMS, and no runtime dependency that can expire and take a page down with it.

Search without a vector database

The original build ran semantic search on Supabase with pgvector. It broke — the free-tier project was reclaimed, DNS stopped resolving, and search returned errors on a site with eighteen documents.

The replacement is a local index: field-weighted TF-IDF with prefix matching and a cosine-similarity pass for "related work". Eighteen documents do not need approximate nearest neighbours. It runs in memory, costs nothing, answers instantly, and cannot break when someone else's service changes.

An assistant that admits what it does not know

The site has a chat assistant, Locus, grounded in the same content the pages render.

The version I inherited had a real failure mode: the prompt described a search_content tool that was never wired up, so the model cheerfully reported searches it had not run. Confident fabrication on a research site is worse than no assistant. Now the corpus is inlined in the prompt, the model has no tools and is told so, and when something is not covered the answer is one sentence: that is not on the site.

It runs on a small model with the request validated, rate-limited per IP, and capped on output. Roughly sixty cents per thousand messages.

Motion with one rule

The interactions use GSAP: a hero timeline, scroll-linked parallax, a character whose eyes track the cursor.

One rule governs all of it, learned by breaking the site three times in the same way. Content renders visible by default. Motion components set their own from-state inside useGSAP, which runs before paint. Nothing is hidden in CSS waiting for JavaScript to reveal it — because if that JavaScript fails, the visitor gets a blank page where a name should be, and no error to explain it. Everything degrades to legible.

prefers-reduced-motion skips the animation entirely.

Invisible work

Structured data — Person, WebSite, ScholarlyArticle — so a crawler can tell a researcher's site from a blog. Canonicals on every page. An RSS feed. A search page kept out of the index, since it generates unbounded near-duplicate URLs from a query parameter.

None of it is visible to a reader. All of it decides whether a reader arrives.

Still open

Per-page share images, so a link to a paper previews as that paper. Co-author lists in frontmatter, which the publication pages should display and the structured data should carry. And the ordinary work of keeping it current.

You Might Also Like