Case study · tool / personal infrastructure
This site — built so I could change it the way I think.
A personal site that doubles as my deployable PHP / MySQL sandbox: inline editing for any content I own, an LLM-backed assistant with retrieval over my own writing, a demos surface for small systems, and admin tooling that treats my own visits as a normal use case.
A site I'd actually maintain.
I wanted a personal site I could keep current without ceremony — no rebuild step to fix a typo, no separate CMS to keep in sync. I also wanted it to be a working environment I could try ideas in: inline editing, retrieval-backed chat, a place to host small interactive demos.
The constraint was that everything had to run on the same boring shared-hosting environment I deploy to, so the architecture stays portable.
Most personal sites are write-once, then ossify.
- Static-site generators turn small edits into rebuild + deploy cycles, which is enough friction to skip.
- Hosted CMS solutions hide the database I want direct access to.
- Bilingual content needs first-class support — fallback to "translate after publish" makes one language always lag.
- I wanted a chatbot that could actually answer questions about my work, not a generic widget.
Designed and built every layer.
-
Owned
Information architecture, design system, all front-end CSS / JS, PHP backend, MySQL schema, auth, admin tooling, chatbot retrieval, deployment.
-
Borrowed
An LLM provider for chat completions; standard PHP libraries for OAuth and PDO; Inter Tight + Noto Sans SC via Google Fonts.
-
Editorial
All copy is mine, EN + ZH, written directly into the live editor instead of an external doc.
Three layers, one database.
- Public site. PHP-rendered pages, scoped CSS modules under
assets/css/pages/, lang-toggle that swaps EN / ZH spans without a re-render. - Inline editor. When I'm signed in as admin, every content node with
data-edit-keybecomes an editable surface that PATCHes to/api/and writes back to MySQL on commit. No separate CMS UI. - Assistant. A chatbot endpoint that retrieves from my own posts and resume content, then completes against an LLM. Knows what I've written, refuses what I haven't.
Same boring stack as Revo, on purpose.
- PHP 8
- MySQL 8
- Vanilla JS (no framework)
- CSS modules per page
- LLM API (chat + retrieval)
- Apache + .htaccess routing
- Inter Tight · Noto Sans SC · SF Mono
Constraints I treated as features.
-
Inline edit, not CMS
Editing happens on the rendered page itself. Removes the round-trip between CMS preview and reality.
-
No build step
CSS and JS are served as-is with mtime-based cache busting. A typo fix is one save, no rebuild.
-
Bilingual at the markup layer
.lang-en/.lang-zhspans live side-by-side. Toggle is a single class swap on<html>. -
CSS scoped per page
Page-specific files live under
assets/css/pages/and only apply whenbodycarries the page class. Adding a new page surface never bleeds into existing ones. -
Database fallback
If MySQL is unavailable, the site renders with cached defaults and the contact form switches to mailto. Personal site, but it shouldn't 500 because the DB blinked.
It's what you're reading.
This page itself is the artifact. The same architecture renders the homepage, blog, resume, and the demos surface.
Edits ship in seconds. Every page on the site is one inline-editor click away from a typo fix.
Bilingual EN / ZH end-to-end, including the chatbot, the resume, and admin tooling.
Demos surface: /demos.php. Public writing: /blog.php.
Open follow-ups.
- Move retrieval from naive substring matching to embeddings — current chatbot recall is good for a small corpus, will degrade as I publish more.
- Add a structured changelog feed that the homepage can pull "last updated" from automatically.
- Extract the inline editor into a small standalone library; right now it's coupled to this site's auth.
- Add a request-level cache layer in front of the resume + settings reads — they hit on every page render.