Vibe coding
Vibe coding my portfolio end to end: Figma, Claude Code, Next.js and Vercel
How I built this site with Claude, from Figma design and planning to tokens, MDX content, GitLab CI and a Vercel deploy. Every step, with what I'd keep.

I rebuilt this portfolio by vibe coding it end to end with Claude: the Figma design, the build plan, a Next.js app driven by design tokens, Markdown content, CI on GitLab and a deploy on Vercel. This post walks through each step in order, with the files and rules that made it work, so you can copy the process for your own site.
Short version: write the plan and the rules down before any code, make Figma the single source of truth, keep the stack boring (no CMS, no database), and let a CI pipeline, not your eyes, decide what reaches production.
What is vibe coding, and what it meant here
Vibe coding is building software by describing what you want to an AI model and steering its output, instead of typing every line yourself. On its own that produces fast demos and fragile code. What made it work for a production site was adding structure around the vibes:
- a written plan the AI works through one phase at a time,
- a design file it reads instead of guessing,
- rules it must follow (tokens only, no invented copy, ask when unsure),
- and an automated quality gate that rejects anything broken.
I stayed the designer and the reviewer. Claude did most of the typing.
The stack at a glance
| Layer | Choice |
|---|---|
| Design | Figma, read by Claude through the Figma MCP server |
| AI tool | Claude Code, working from CLAUDE.md, PLAN.md and docs/ |
| Framework | Next.js (App Router), React, TypeScript in strict mode |
| Styling | Tailwind CSS v4, driven by --dds-* CSS variables |
| Content | MDX files in Git, frontmatter validated with zod |
| Motion | GSAP + ScrollTrigger, respecting prefers-reduced-motion |
| CI | GitLab CI: lint, typecheck, build on every push and MR |
| Hosting | Vercel through its GitLab integration |
Step 1: Design the site in Figma
Everything starts in one Figma file. It holds nine page templates (home, work, case study, blog index, article, services, about, contact, 404), each drawn at three breakpoints: desktop 1440, tablet 768 and mobile 390. The global nav and footer are shared, and a separate page documents the motion spec as dev notes next to each frame.
To be updated.
Two choices in the file paid off later:
- Variables, not raw values. Colours, spacing, radii, type sizes and breakpoints are Figma variables, organised in collections (primitives, semantic light/dark, component). That made them exportable as code.
- A strong visual rule. Apple-style restraint, flat single colours, and no gradients on banners or heroes. A rule like that is easy for an AI to follow and easy for me to check.
The nav is designed like the iOS Dynamic Island: a compact pill that morphs open into a menu. It has its own Figma page with every state and a motion spec.
Step 2: Plan before you prompt
Before any code, Claude and I wrote the plan as files in the repo:
CLAUDE.md: project rules Claude reads at the start of every session (stack, folder structure, conventions, design rules, "don't invent copy").PLAN.md: eight phases, from setup to deploy, each a checklist. The definition of done for every phase ispnpm lint && pnpm typecheck && pnpm buildpassing.docs/: one spec per topic: design system, content model, pages, animation, CI and deploy.
The plan also changed my mind about the architecture. The first version of this project was a monorepo with a separate Strapi CMS backend. Writing the plan made it obvious that a one-person portfolio doesn't need a CMS, a database or an API. The rewrite became one Next.js app with content as plain files in Git. That deleted a whole backend before it existed.
Step 3: Set up the project
Phase 0 was the boring part, and the most important one to get right:
pnpm create next-appwith TypeScript, Tailwind, App Router and ESLint- Prettier with the Tailwind plugin, plus
typecheckandformatscripts - Only the dependencies the plan named:
gray-matter,zod,next-mdx-remote,gsap,@gsap/react, and the remark/rehype plugins for headings and tables - The folder structure from
CLAUDE.md,.env.example,.nvmrcand a CI file
One rule in CLAUDE.md keeps this lean: Claude can't add a dependency without saying why.
Step 4: Turn Figma variables into design tokens
Claude pulled the variables straight from Figma with the MCP tool get_variable_defs and wrote them to styles/tokens.css. Naming follows --dds-[type]-[value] in three tiers:
- Core primitives: raw values, like
--dds-color-blue-500or--dds-space-16 - Semantic aliases: meaning, like
--dds-color-text-primary - Component tokens: one component, like
--dds-button-height
Tailwind v4's @theme then exposes only these tokens. The default palette, spacing scale and type scale are cleared, so a hard-coded colour or font size simply has no utility class to use. Dark mode overrides the semantic tier through data-theme="dark", so components never change.
- Token definitions
- 519
- Breakpoint frames
- 3
- Page templates
- 9
- Gradients
- 0
Step 5: Build the shell, then the pages
With tokens in place, the order was: primitives (Button, Tag, Container, Section), then the layout shell (root layout, footer, 404, Dynamic Island nav), then pages.
For each page, Claude reads the Figma frame with get_design_context, builds it from the shared components, and screenshots it at 1440, 768 and 390 to compare with the design. I review it in the browser, send small tweaks (a size, an animation order, a colour), and only then say "commit and continue". One page per loop keeps every change small enough to review properly.
Motion follows the same pattern. GSAP and ScrollTrigger drive the sticky and parallax sections, animating only transform and opacity, and every effect has a reduced-motion fallback.
Step 6: Content as MDX files, validated at build time
There's no CMS. Case studies live in content/work/*.mdx and posts like this one in content/blog/*.mdx. A typed loader in lib/content.ts reads them with gray-matter and checks the frontmatter with zod.
The useful part is what happens when something is wrong. A summary over 160 characters, a date that isn't YYYY-MM-DD, or a cover image that doesn't exist in public/ throws an error with the file path and field name. That fails pnpm build, which fails CI, which blocks the deploy. Bad content can't reach production.
const postSchema = z.strictObject({
title: z.string().min(1),
summary: z.string().min(1).max(160, "Keep the summary to 160 characters or fewer"),
date: isoDate,
tags: z.array(z.string().min(1)).default([]),
draft: z.boolean().default(false),
});
Writing a post is now: create an .mdx file, push a branch, open a merge request.
Step 7: CI on GitLab, deploy on Vercel
The pipeline has two separate jobs, owned by two separate systems:
- GitLab CI is the quality gate. One
checkjob runspnpm lint,pnpm typecheckandpnpm buildon every merge request and branch push, with the pnpm store and.next/cachecached. - Vercel does the deploy. Through its GitLab integration, every merge request gets a preview URL and every merge to
maingoes to production at lexuanhau.dev (opens in a new tab).
With "Pipelines must succeed" enabled on merge requests, nothing reaches main, and so nothing reaches production, without a green pipeline.
To be updated.
What I'd keep for the next project
- Write the rules before the code.
CLAUDE.mdturned "make it look nice" into checkable constraints. - Make the design file the source of truth. When Figma and the docs disagree, Claude has to ask instead of choosing.
- Ban invented content. Unknown copy, links and project details become clearly marked
TODO:placeholders, listed at the end of every task. - Cut infrastructure early. Dropping the CMS removed more work than any prompt could save.
- Let the build be the reviewer for correctness. Types, lint and content validation catch what screenshots miss, so my reviews can focus on design.
To be updated.
FAQ
Can you build a production website with vibe coding?
Yes, if the AI works inside clear limits: a written plan, a design to match, coding rules, and an automated build that rejects broken code. Without those, vibe coding is good for prototypes but hard to maintain.
Do I need a CMS for a personal portfolio?
Usually not. MDX files in Git, validated at build time, give you versioning, previews on every merge request and no extra service to host or secure.
How does Claude read a Figma design?
Through the Figma MCP server. Claude Code calls tools like get_variable_defs to export variables as design tokens and get_design_context to read a frame's layout and styles before building it in code.
What does the deploy pipeline look like?
Push a branch, open a merge request, GitLab CI runs lint, typecheck and build, Vercel posts a preview URL, and merging to main deploys to production.