Back to selected work

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.

  • Role Designer + sole engineer
  • Period 2024 – present
  • Stack PHP · MySQL · Vanilla JS · LLM API
  • Status Live
Context

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.

Problem

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.
My role

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.

System design

Three layers, one database.

  1. Public site. PHP-rendered pages, scoped CSS modules under assets/css/pages/, lang-toggle that swaps EN / ZH spans without a re-render.
  2. Inline editor. When I'm signed in as admin, every content node with data-edit-key becomes an editable surface that PATCHes to /api/ and writes back to MySQL on commit. No separate CMS UI.
  3. 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.
Technical stack

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
Key decisions

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-zh spans 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 when body carries 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.

Outcome / evidence

It's what you're reading.

Proof

This page itself is the artifact. The same architecture renders the homepage, blog, resume, and the demos surface.

Cadence

Edits ship in seconds. Every page on the site is one inline-editor click away from a typo fix.

Reach

Bilingual EN / ZH end-to-end, including the chatbot, the resume, and admin tooling.

Try it

Demos surface: /demos.php. Public writing: /blog.php.

What I'd improve next

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.