Note

Building qiannawang.com: A Full-Stack Personal Portfolio Case Study

Aug 13, 2026

Experience + tutorial: building a CMS-backed portfolio — tone, stack, Supabase, Vercel, SEO/GEO, and analytics.

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:

RolePurpose
PortfolioProjects + Traces (photography, drawings, field notes)
WritingNotes for tools, experiments, deployment guides
IdentityAbout timeline + 16:9 profile photo; Hero one-liner on home
Light interactionModerated guestbook; analytics on what readers open

Positioning & Content Strategy

Content modules

ModulePathNatureUpdate cadence
Notes/notesLong-form Markdown, bilingualOccasional
Projects/projectsThesis / architecture / digital productPer project
Traces/tracesPhotography, drawings, outdoor field notesAccumulation
About/aboutBackground, timeline, working acrossLow
Guestbook/guestbookVisitor 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

  1. Quality over quantity
  2. Text + images both matter (overview paragraphs help SEO / GEO)
  3. English-first on lists; bilingual toggle on note detail
  4. Legacy paths kept (/photography, etc.) alongside /traces hub

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.

TypeWhereWhat to look at
Curated webAwwwards, Godly, siteInspireTypography, restrained motion, hero density
Portfolios / photoBehance, Dribbble, architect or photographer sitesProject storytelling, gallery pace, About pages
Journal / blog feelReadymag, Cargo sites, indie magazinesHeading hierarchy, section spacing, lists + longform
UI patternsMobbin (app-heavy, still useful)Sidebars, tabs, mobile drawers

My workflow:

  1. Screenshot + annotate what works (and what feels too loud)
  2. Write a short mood brief — e.g. stone warm gray, serif titles, archive not SaaS
  3. Discuss with AI in Cursor — share screenshots + brief; ask for layout direction before dumping component libraries
  4. Static pass first — app/_data home 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, body text-stone-700, headings text-stone-900
  • Section labels: text-xs tracking-[0.24em] uppercase text-stone-500
  • Fonts: Geist Sans (body), serif stack for font-serif headings
  • Hero: full-width, CMS replaceable, migrated as WebP via sharp
  • About photo: 16:9 only on /about, cropped in admin with react-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/image with Supabase remotePatterns; gallery components with captions and sort order
  • Admin: direct upload; About photo cropped to 16:9 via react-easy-crop
  • Local dev: sparse-checkout may omit public/ assets — run scripts/download-*-media.ps1 before migrate

Long video (project intros, field notes)

  • Rule: large, rarely changed videos stay out of Supabase Storage
  • Hosting: Google Drive /preview embed 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

MediaTypical usePipelineOn site
MarkdownNotes, overviewsAdmin → DBMarkdown + TOC
PhotosHero, galleriessharp → Storage → DBnext/image, galleries
Long videoIntros, tripsDrive URL → DBiframe 16:9
MixedNarrative field notesimages Storage, video externalstory 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

LayerChoiceVersion
FrameworkNext.js App Router16.2.1
UIReact19.2.4
LanguageTypeScript5.x
StylingTailwind CSSv4
BackendSupabase (Postgres, Auth, Storage)—
HostingVercelProduction + Preview
Markdownreact-markdown + remark-gfmNotes

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 + cookie qn_vid → page_views table

Admin: How I Manage My Own Content

  • Layout: AdminShell — fixed sidebar, collapse state in localStorage
  • /admin redirects 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

DomainRole
www.qiannawang.comPrimary
qiannawang.comRedirect to www
qianna-site.vercel.appFallback

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

SourceMetrics
page_viewsPV, UV, dwell time by content
Vercel AnalyticsTraffic, referrers
Speed InsightsCore 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 / build after 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:

MCPWhat it doesTypical build-time use
SupabaseTables, logs, migration hints, RLS checksConfirm CMS columns after a new table; Postgres logs when Guestbook / Analytics misbehaves; list_tables before writing SQL
GitHubPRs, issues, CI status, gh-style workflowsSee if the push is green; compare remote commits; draft PR summaries
VercelDeployments, env vars, domains, deploy logsProduction up to date? NEXT_PUBLIC_* set? Domain shows Valid Configuration?

Practices that work for me:

  1. 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
  2. Keep humans on sensitive actions — production migrations, env changes, deletes: MCP informs; I confirm
  3. Split roles with local scripts — bulk image migration stays on npm run migrate:* + sharp; MCP shines for status checks, config alignment, production debugging
  4. 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 in hero-distortion-config.ts
  • Dev tuner: only when NODE_ENV === development — HeroDistortionTuner sliders (wave speed, refraction, wake angle, etc.); saves to localStorage; copy JSON back into defaults
  • A11y: prefers-reduced-motion falls 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

  1. Static pages + app/_data until tone and content feel right
  2. CMS the module you update most (I started with Projects)
  3. 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.

← Back to Notes