Why I built this
I wanted one place that's truly mine — not just a static résumé, but somewhere I can keep adding to: projects I ship, things I learn, and the occasional reflection. This blog is where I'll record the day-to-day — what I'm building, what broke, and what I'd do differently. Think of it as a running log rather than a finished portfolio.
What it runs on
The site is split into a frontend and a backend:
- Frontend — Next.js (App Router) + TypeScript + Tailwind CSS, deployed on Vercel. It's bilingual (EN / 中) with a language toggle, renders Markdown content, and uses ISR so pages stay fast and fresh.
- Backend — FastAPI + SQLAlchemy (async) + Alembic, managed with uv. It owns all the data, serves a small JSON API the frontend calls, and now also runs the RAG chat assistant below.
- Database — Supabase (PostgreSQL) with the pgvector extension.
The data model (a peek)
Everything you see is data, not hardcoded:
- projects — bilingual title / summary / body (Markdown), a tech list, links (JSON), an optional demo video, GitHub stars / forks, and a date used to order newest-first.
- posts — bilingual title / excerpt / body, tags, and reading time. This very post is a row in that table.
- doc_chunks — a pgvector table of embedded content chunks that now powers the chat assistant. Each chunk also stores a content hash, which is the key to the sync trick below.
Meet the assistant
There's now a small chat widget on the site, built with LangGraph and Gemini. Ask it something about me, a project, or a post, and it streams back a grounded answer with links to the right page — in whichever language (EN / 中) you're asking in.
Under the hood it's a tiny 3-node graph:
- Condense — rewrite a follow-up question into a standalone search query, resolving references like “that one” using the conversation so far.
- Retrieve — embed the query and pull the closest chunks from
doc_chunksvia pgvector's cosine search, scoped to the current language. - Generate — stream an answer grounded only in those chunks, citing sources as Markdown links. If the context doesn't cover the question, it says so instead of guessing.
Keeping the index in sync
The trickiest part of any RAG setup is keeping the vector index honest as content changes. doc_chunks now syncs incrementally: every source (each project, post, and the profile blurb, in each language) is hashed, and only sources whose hash changed get re-chunked and re-embedded — unchanged ones are left alone. Anything removed or unpublished has its chunks deleted too. This runs after seeding and as a background task when the API starts, so the assistant's knowledge stays current without me remembering to run anything by hand. A --full flag is still there if I ever need to force a complete rebuild (say, after changing the chunker or embedding model).
Why this architecture
A few deliberate choices, holding up well so far:
- A separate Python backend. Python is my home turf (FastAPI, SQLAlchemy), and it turned out to be the natural home for the LangGraph assistant above. A clean API boundary keeps the frontend simple.
- Next.js on Vercel. Great developer experience, server rendering + ISR for speed and SEO, and effortless deploys.
- Supabase + pgvector. One database for content, embeddings, and now content hashes — the assistant and the incremental sync both live on top of it with zero extra infrastructure.
- Content as data. Storing projects and posts in the DB (instead of hardcoding them) lets me add or edit things without redeploying, and the assistant picks up the changes on its own.
What's next
With the assistant live, the next focus is tuning retrieval quality as more projects and posts land, and writing more of these notes as I go.