Building qiannawang.com: A Full-Stack Personal Portfolio Case Study
An experience + tutorial-style long read: how I turned www.qiannawang.com from static pages into a CMS-backed portfolio — content choices, visual tone, stack decisions, Supabase + Vercel, SEO / GEO, and analytics.
Repo:24205345/qianna-website· Last updated: 2026-08-13
Preface: Why This Site Exists
A portfolio can be a one-page landing site, or an online CV. I wanted something closer to a small archive / journal —
- Quiet to read, like a curated sketchbook + research archive
- CMS-backed so copy edits do not always need a redeploy
- Next.js + Supabase + Vercel, kept practical
Four roles in one:
| Role | Purpose |
|---|---|
| Portfolio | Projects + Traces (photography, drawings, field notes) |
| Writing | Notes for tools, experiments, deployment guides |
| Identity | About timeline + 16:9 profile photo; Hero one-liner on home |
| Light interaction | Moderated guestbook; analytics on what readers open |
Positioning & Content Strategy
Content modules
| Module | Path | Nature | Update cadence |
|---|---|---|---|
| Notes | /notes | Long-form Markdown, bilingual | Occasional |
| Projects | /projects | Thesis / architecture / digital product | Per project |
| Traces | /traces | Photography, drawings, outdoor field notes | Accumulation |
| About | /about | Background, timeline, working across | Low |
| Guestbook | /guestbook | Visitor messages (moderated) | As visitors arrive |
Home narrative order
Notes → Projects → Traces → About Me (with 3 guestbook previews)
Recent writing first, then projects, then traces, then about.
Hero: cover image, title, subtitle, and a lightweight Enter → link. No big conversion button.
Content principles
- Quality over quantity
- Text + images both matter (overview paragraphs help SEO / GEO)
- English-first on lists; bilingual toggle on note detail
- Legacy paths kept (
/photography, etc.) alongside/traceshub
Visual Style & Design System
Early on: finding UI references
Before coding, I browsed sites, took screenshots, and wrote down the mood I wanted. Portfolios benefit from references that feel editorial, quiet, and image-led — not only other designer portfolios.
| Type | Where | What to look at |
|---|---|---|
| Curated web | Awwwards, Godly, siteInspire | Typography, restrained motion, hero density |
| Portfolios / photo | Behance, Dribbble, architect or photographer sites | Project storytelling, gallery pace, About pages |
| Journal / blog feel | Readymag, Cargo sites, indie magazines | Heading hierarchy, section spacing, lists + longform |
| UI patterns | Mobbin (app-heavy, still useful) | Sidebars, tabs, mobile drawers |
My workflow:
- Screenshot + annotate what works (and what feels too loud)
- Write a short mood brief — e.g. stone warm gray, serif titles, archive not SaaS
- Discuss with AI in Cursor — share screenshots + brief; ask for layout direction before dumping component libraries
- Static pass first —
app/_datahome order until reading flow feels right, then CMS
References supply principles, not pages to clone. This site’s stone palette + serif titles came from a few rounds of that.
Quiet editorial · Stone warm gray · Serif titles · Uppercase tracked section labels
- Background:
bg-stone-50, bodytext-stone-700, headingstext-stone-900 - Section labels:
text-xs tracking-[0.24em] uppercase text-stone-500 - Fonts: Geist Sans (body), serif stack for
font-serifheadings - Hero: full-width, CMS replaceable, migrated as WebP via sharp
- About photo: 16:9 only on
/about, cropped in admin withreact-easy-crop - Field note large videos: Google Drive preview URLs (too large for Supabase Storage)
Content & Media: process and presentation
The site mixes copy, high-res photos, and occasional long video. Rough split: text and photos through CMS + Storage; long video via embed URLs; Hero treated separately.
Text (Notes, project copy, About)
- Storage: Supabase tables (
notes.body_markdown,projects.overview_paragraphs, etc.) - Editing: bilingual Markdown in Admin; Notes attachments to
portfolio-media - Public:
react-markdown+ GFM; auto TOC on note detail - SEO: SSR HTML + per-page
generateMetadata
High-res photos (Hero, galleries, Traces)
- Source: local JPG/PNG (Hero originals can be tens of MB)
- Processing: sharp in migrate scripts — Hero ~2400px wide WebP q≈82; batch
npm run migrate:*for galleries - Storage: public Supabase bucket
portfolio-media - Public:
next/imagewith SupabaseremotePatterns; gallery components with captions and sort order - Admin: direct upload; About photo cropped to 16:9 via
react-easy-crop - Local dev:
sparse-checkoutmay omitpublic/assets — runscripts/download-*-media.ps1before migrate
Long video (project intros, field notes)
- Rule: large, rarely changed videos stay out of Supabase Storage
- Hosting: Google Drive
/previewembed URLs - DB:
intro_video_url,hero_video_url,field_note_media(video_external) - Public: 16:9
<iframe>on detail templates - Admin: paste Drive preview URL in project form or Field Notes MediaManager
Summary
| Media | Typical use | Pipeline | On site |
|---|---|---|---|
| Markdown | Notes, overviews | Admin → DB | Markdown + TOC |
| Photos | Hero, galleries | sharp → Storage → DB | next/image, galleries |
| Long video | Intros, trips | Drive URL → DB | iframe 16:9 |
| Mixed | Narrative field notes | images Storage, video external | story blocks + video |
Information Architecture & Page Templates
Once the visual tone is set, routes and page templates follow.
Public routes
/ /notes /notes/[slug] /projects /projects/[slug] (layouts: thesis | xicaoshi) /traces /photography /visual-works /field-notes /field-notes/[slug] (layouts: gallery | narrative) /about /guestbook
Admin sidebar (2026-08)
Fixed left sidebar, collapsible on desktop, drawer on mobile:
Site · Notes · Projects (categories) · Traces (3 items) · About · Guestbook · Analytics
Tech Stack Choices
| Layer | Choice | Version |
|---|---|---|
| Framework | Next.js App Router | 16.2.1 |
| UI | React | 19.2.4 |
| Language | TypeScript | 5.x |
| Styling | Tailwind CSS | v4 |
| Backend | Supabase (Postgres, Auth, Storage) | — |
| Hosting | Vercel | Production + Preview |
| Markdown | react-markdown + remark-gfm | Notes |
Adding CMS later
Early on, static app/_data/*.ts was enough. As content grew, Supabase helped with browser-based edits, RLS for published-only public reads, and one place for media. Static fallback in app/_data still keeps builds working when Supabase is unavailable.
Hosting on Vercel
I also wrote an ECS deploy note for a different setup. This portfolio stays on Vercel: git push to deploy, good Next.js integration, HTTPS and domains without much ops.
System Architecture
Four-layer CMS pattern
Every content module on this site follows the same shape:
supabase/migrations/ → schema + RLS app/_data/ → static fallback lib/<module>/queries.ts → fetch + fallback app/<module>/ → Server Components app/admin/<module>/ → CRUD + Server Actions scripts/migrate-* → local media → Storage → DB
Rendering: Browser → Server Component → queries → Supabase (anon + RLS) → fallback if empty → HTML (SEO-friendly)
Auth: middleware.ts guards /admin/*. Supabase email auth; redirect URLs must include www, apex, and vercel.app fallback.
Database & Supabase CMS
- Project ID:
aqsdwfocoocnzyxopvvg - Bucket:
portfolio-media(public read) - Migrations:
0001–0016
Key tables: projects, photography_*, visual_work_*, field_notes, site_settings, site_navigation_items, about_page_content, notes, guestbook_messages, page_views.
RLS: anon sees published content only; authenticated admin full CRUD.
Sparse-checkout: large public/ assets may be missing locally — download scripts before migrate.
Frontend Implementation
- Notes: Markdown + auto TOC + EN/ZH toggle
- Projects: category filters via
lib/projects/categories.ts - Traces hub: tabs share components under
app/_components/traces/ - Guestbook: name + optional email (admin-only) + message; approve before public
- Tracking:
PageViewTracker+ cookieqn_vid→page_viewstable
Admin: How I Manage My Own Content
- Layout:
AdminShell— fixed sidebar, collapse state in localStorage /adminredirects to/admin/projects- Modules: Site, Notes, Projects, Traces (3), About (16:9 crop), Guestbook moderation, Analytics charts
Private CMS for my own edits only.
Deployment, Domain & Environment
Next up: deployment and domain, then SEO / GEO, then analytics. I added these in stages; day one can be “just works in the browser.”
Deploy: git push main → Vercel Production
| Domain | Role |
|---|---|
www.qiannawang.com | Primary |
qiannawang.com | Redirect to www |
qianna-site.vercel.app | Fallback |
Env vars (Vercel Production): NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_SITE_URL=https://www.qiannawang.com — redeploy after changing NEXT_PUBLIC_*.
SEO & GEO Optimization
In Aug 2026 a guestbook SEO pitch prompted a check: root metadata still said "Create Next App". Fixed with site + page metadata, sitemap / robots, JSON-LD (WebSite + Person), and public/llms.txt.
GEO: clear About copy, Person schema, summarizable intros, llms.txt.
Post-deploy: Google Search Console → submit sitemap.xml. Cold-call SEO services can be ignored if metadata, About, and sitemap are in good shape.
Visitor Analytics
| Source | Metrics |
|---|---|
page_views | PV, UV, dwell time by content |
| Vercel Analytics | Traffic, referrers |
| Speed Insights | Core Web Vitals |
Guestbook moderation also catches spam SEO pitches — reject and move on.
Appendix: Lessons From the Build Process
Notes from building my own portfolio — patterns you might borrow for yours.
Product before framework
I wrote the product chapters before the stack chapters on purpose. Up front I cared most about home page reading order and quiet vs. loud. Tone sticks; stack can change.
Iterate in small slices
Projects, Traces, Notes, Guestbook, and Analytics landed over time. Each module went through migration → queries → public page → admin. The reusable pattern mattered more than shipping every feature at once.
Working with AI (Cursor)
A few habits that helped:
- One concrete goal per session (e.g. “add 16:9 crop for About photo”)
- Read existing code before editing
- Run
lint/buildafter changes — Next.js 16 APIs differ from older docs - Tone, IA, guestbook policy — still my calls
Use MCP: wire Supabase, GitHub, and Vercel into Cursor
When pairing with AI on code, MCP (Model Context Protocol) saves a lot of back-and-forth: the agent reads local files and, with your permission, checks live cloud state — fewer screenshots and pasted query results.
What I use for this stack:
| MCP | What it does | Typical build-time use |
|---|---|---|
| Supabase | Tables, logs, migration hints, RLS checks | Confirm CMS columns after a new table; Postgres logs when Guestbook / Analytics misbehaves; list_tables before writing SQL |
| GitHub | PRs, issues, CI status, gh-style workflows | See if the push is green; compare remote commits; draft PR summaries |
| Vercel | Deployments, env vars, domains, deploy logs | Production up to date? NEXT_PUBLIC_* set? Domain shows Valid Configuration? |
Practices that work for me:
- Connect MCP before long tasks — e.g. “add Analytics tables”: let the agent read the current Supabase schema first, then write migrations instead of guessing column names
- Keep humans on sensitive actions — production migrations, env changes, deletes: MCP informs; I confirm
- Split roles with local scripts — bulk image migration stays on
npm run migrate:*+ sharp; MCP shines for status checks, config alignment, production debugging - Pair with doc MCPs when needed — Next.js 16 APIs vs. old blog posts: Context7 (or similar) plus Supabase/Vercel MCPs cover different gaps
Setup: Cursor Settings → MCP → add the Supabase / GitHub / Vercel server (marketplace plugin or self-hosted). Configure once; every session the agent can use them without you re-pasting project IDs or deployment URLs.
The site as a small lab: hero ripples
A portfolio can host experiments that have nowhere else to go. The clearest example here is the home Hero water-ripple distortion:
- Implementation:
HeroImageDistortion.tsx— WebGL shader on the Hero texture; mouse move/click creates ripples and wakes; tunables inhero-distortion-config.ts - Dev tuner: only when
NODE_ENV === development— HeroDistortionTuner sliders (wave speed, refraction, wake angle, etc.); saves tolocalStorage; copy JSON back into defaults - A11y:
prefers-reduced-motionfalls back to a static<img> - Production: tuned effect only — no tuner panel
Pattern: one client component + config file + dev-only tuning UI. Future tries — scroll-driven layouts, new Traces viewers, note code themes — can ship small on a personal site, keep or drop without a client deadline.
Process notes for future me
Short docs/exec-*.md files in the repo are memos for later me. Public write-ups live in Notes, like this one.
If you build something similar
- Static pages +
app/_datauntil tone and content feel right - CMS the module you update most (I started with Projects)
- Domain and SEO can wait; just fix scaffold metadata in layout early
Your rhythm may differ — this was mine.
Lessons & Next Steps
Wins: CMS + fallback dual track; four-layer modules; editorial UI; moderated guestbook; SEO in code review.
Pitfalls: sparse-checkout missing images; Supabase redirect URLs; Next 16 await params; never re-export server actions.
Optional next: Search Console; LinkedIn in Person sameAs; RSS for Notes.
Closing
This site will keep evolving. If you're building a portfolio too, settle on reading order and tone before picking a stack. Leave a message — it shows up after review.