An honest build log: how we went from a spoken brief to a portfolio in production with its own CMS — first designing the brand with Claude Design, then building and deploying it with Claude Code.

This document mixes the narrative (decisions, back-and-forth) with the technical (stack, structure, deploy, workflow). The idea is for it to both tell the story and let someone replicate it.


01

Before the code: the brand, designed with Claude

Nine years designing products, and still the designer's portfolio is the designer's worst project: too close, too personal, with no brief to save you from your own doubts. Instead of opening Figma and fighting a blank canvas, I opened a conversation with Claude Design and spoke to it the way I'd speak to a studio: who I am, what I admire, where I want to go.

I didn't write a prompt. I wrote a brief. I told it I'm a senior designer aiming at global teams, that my sensibility is minimalist, that I'm obsessed with Japanese art and the concept of Ma (間) — the space between things. The difference with a traditional tool was immediate: it didn't ask me to pick a template, it gave me back questions (one direction or several? novel or build on an existing system? how interactive?) — exactly the questions I ask my clients. The quality of the output was, exactly, the quality of the brief.

Three worlds in an afternoon, not three weeks

Instead of one proposal, Claude built three complete directions — each with its philosophy, typography, palette, components and an applied screen — and put them side by side on a single canvas to compare:

Ma · the space between
Editorial silence. Sumi ink on washi paper. A single hairline as ornament, one drop of vermilion where the eye lands.
the one I chose
Sōsho · cursive script
Asymmetric rhythm, Japanese vermilion, the gesture of a brushstroke. Expressive, alive, with a hanko seal.
Yūgen · profound grace
Refined dark mode. Bronze on warm charcoal. Cinematic, staff-level.

All three were defensible. That was the uncomfortable, revealing moment: I wasn't choosing between one good option and two fillers, I was curating. And curating —deciding what to leave out— is still the hardest and most human part of design.

I chose the quietest one — Ma. Not because it was the flashiest (it isn't), but because it said something true about how I work: I remove three things for every one I add. I asked for a single variant — that the palette breathe a little beyond vermilion — and it came back with an expanded but disciplined color system: each color with a defined usage budget (ochre 黄土 reserved for awards). It didn't invent colors at random: it extended the existing harmony.

From system to navigable site, without switching tools

This is where it stopped looking like an image generator. I asked for the full design system and it returned a functional site: home with a narrative scroll, a projects archive, a case study, about, journal, contact and —my favorite part— a page that documents the design system itself (type scale, tokens, components, rules). For the identity it built a lab with six logotypes —from an editorial wordmark to an LV monogram with a ligature, a vermilion hanko seal and a mark that made the concept of Ma literal— each tested large, at nav size, and reversed.

"The AI gave me three correct answers. Choosing which one was mine still depended on nine years of judgment."

— margin note, day one

The AI didn't give me the brand: I chose it from the options it offered. The tool filled the space of making; I kept the space of deciding. And that, in the end, is still where design lives.


02

The code's starting point: the "Ma" export

Out of that conversation came the concrete input for Claude Code: a Claude Design export — an HTML prototype (Ma System.html) that loaded React via CDN + Babel standalone and a few loose .jsx files.

The pieces of the visual system that arrived packaged:

- Washi (paper) palette: paper #f4f1ea, ink #1a1815, and accents spent like money — vermilion 朱 #9b3a2a, indigo 藍 #2d3f5c, matcha 抹茶 #6b7a4d, ochre 黄土 #a87f3a. - Typography: Fraunces (display serif), Inter Tight (UI sans), JetBrains Mono (metadata), Shippori Mincho + Noto Serif JP (kanji). - Interactions: custom cursor, scroll reveals, lines that "draw" themselves (hairline-draw), a quiet marquee.

The prototype was pretty but it wasn't a real site: no real content, no way to edit it, and no way to deploy it. That was Claude Code's job — and everything that follows.


03

The stack (and why)

PieceChoiceWhy
FrameworkNext.js 14 (App Router)SSR, folder routing, React Server Components
UIReact 18What the prototype already used
StylesInline styles + tokens + a CSS layerThe prototype was inline; we respected it
Fontsnext/font/googleAuto self-host, no FOUT, CSS variables
DatabasePostgreSQL on NeonServerless-friendly (key for Vercel)
ORMPrisma 5Declarative schema, migrations, type-safety
Admin sessioniron-sessionSigned cookies, simple
HostingVercelDeploy from git, ideal for Next.js
DomainNamecheap → DNS to VercelThe domain was already bought

04

Project structure

src/
  app/
    (public)/            # public site
      layout.tsx         # Nav + Footer + Cursor + ScrollReveal
      page.tsx           # Home
      work/page.tsx      # Projects archive
      work/[slug]/       # Case study (configurable template)
      studio/page.tsx    # About
      journal/page.tsx   # Article list
      journal/[slug]/    # Article (markdown render)
      contact/page.tsx
      system/page.tsx    # Design system documentation
    admin/               # Admin panel (CRUD)
    api/admin/           # Admin API routes
  components/            # Nav, Footer, Cursor, ScrollReveal, Wordmark, forms
  lib/
    tokens.ts            # The T object with colors and fonts
    db.ts                # Prisma client (singleton)
    session.ts           # iron-session
    markdown.tsx         # Dependency-free markdown render
prisma/
  schema.prisma          # Models: Project, JournalPost, SiteContent
  seed.mjs               # Initial content (idempotent, with upsert)
public/                  # Static images

A recurring idea: content lives in the database, not hardcoded. Pages are export const dynamic = 'force-dynamic' and read from Prisma on each request. That way the admin panel can edit everything without touching code.


05

The CMS and the admin panel

Three models in schema.prisma:

- `Project` — projects and case studies. Includes fields for the configurable template: heroEm, heroTail, problemIntro, resultsIntro, heroImage, gallery (JSON), problems/approach/results/learnings (JSON strings). - `JournalPost` — articles: title, slug, date, subtitle, content (markdown), readMinutes. - `SiteContent`key/value (JSON) pairs for editable site copy: hero, philosophy, trajectory, footer, about.

The panel lives at /admin (password login via iron-session). It has CRUD for projects and posts, and editing of site content. The API routes at /api/admin/* validate the session before writing.

Seed trick: prisma/seed.mjs uses upsert for each record, so running it many times never duplicates anything. And a deleteMany({ where: { slug: { notIn: [...] } } }) clears what's no longer in the seed. This lets us version the base content in git and re-seed on each deploy.


06

Deploy: Neon + Vercel + domain

6.1 Database (Neon)

1. Create a project at neon.tech → copy the connection string (postgresql://...?sslmode=require). 2. In schema.prisma, provider = "postgresql".

6.2 Connect to Vercel

1. Push the repo to GitHub. 2. Import the repo at vercel.com/new → detected framework: Next.js. 3. Environment variables (in all three environments): - DATABASE_URL → the Neon string - SESSION_SECRET → a long random string - ADMIN_PASSWORD → the panel password

The build script does the heavy lifting:

json
"build": "prisma generate && prisma db push && node prisma/seed.mjs && next build"

That is: generate the client, sync the schema with Neon, seed the content, and only then compile Next.

6.3 Domain (Namecheap)

In Advanced DNS, an A record @ → 76.76.21.21 (the IP Vercel gives you) + the verification TXT. It propagates in minutes; Vercel issues the SSL on its own.


07

Real content (no placeholders)

The prototype came with invented projects and testimonials. We replaced them with truthful content:

- Case studies (Alimarket / Alicorp and Apolo Design System / SimpliRoute) imported from a Webflow CSV export. The case-study template was made configurable (heroEm, problemIntro, etc. fields) instead of hardcoding the placeholder copy. - Bio and trajectory rewritten from the real CV (Valere, Banco Pichincha, Buk, Alicorp, SimpliRoute). - Honest cleanup: we removed an invented testimonial and fake awards the placeholder shipped with. A live portfolio shouldn't carry credentials that aren't real.


08

The Journal and a dependency-free markdown render

We imported real articles from the LinkedIn data export (Settings → Data Privacy → Get a copy of your data → Articles). The export is HTML; we extracted the text and turned it into markdown.

To show them we built:

1. The detail page journal/[slug]/page.tsx. 2. A custom markdown renderer (lib/markdown.tsx) — no libraries, to avoid touching the package-lock. It supports ##/###, > quotes, lists, and inline **bold**, *italic*, [links](url). The idea: parse by blocks (separated by blank lines) and then inline with a single regex.

tsx
// Each block: heading / blockquote / list / paragraph.
const blocks = text.trim().split(/\n{2,}/);
// Inline: one regex capturing link | bold | italic | code, in order.
const pattern = /(\[([^\]]+)\]\(([^)]+)\))|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)|.../g;

And a reading-progress bar that fills as you scroll, pinned to the Nav (which is position: fixed) and measured with window.scrollY against the <article> bounds.


09

Responsive: one layer, marker classes

The prototype was all inline styles, where you can't put media queries. Instead of rewriting every component, we added a responsive layer in `globals.css` with marker classes and !important overrides:

css
/* Inline keeps the desktop layout; this takes over on mobile */
@media (max-width: 900px) {
  .r-stack { grid-template-columns: 1fr !important; }
}
@media (max-width: 640px) {
  .r-pad { padding-left: 20px !important; padding-right: 20px !important; }
}

Then we gave each key container its class (r-pad, r-stack, r-hero, r-2/3/4, r-row). Plus:


10

The workflow with Claude Code

The process was conversational, but backed by a very concrete technical loop: each natural-language request ended in a commit, and each commit in a deploy. The cycle, in detail:

1. Request → tight diff. Each instruction ("raise the image a bit", "drop the scroll background", "the title is too long") translated into a small, surgical change — most touching a single file. Keeping the diff tight makes the commit readable and a rollback trivial (git revert <sha>).

2. Local verification before pushing. The production DB (Neon) isn't reachable from the sandbox, so to test we ran the project against a temporary SQLite:

bash
# schema with sqlite provider + ephemeral db, just to verify render
npx prisma db push && node prisma/seed.mjs
npm run dev &
curl -s localhost:3000/journal/<slug> | grep -c 'reading-progress'

Confirming the markup with curl (instead of "looks fine") caught several false positives: things that rendered on first load but not after navigating.

3. Atomic, descriptive commits. One change = one commit, with a message that explains the why, not just the what:

fix(journal): pin progress bar to the Nav (fixed) and measure with scrollY
feat(work): configurable case-study template (heroEm, problemIntro)
content(studio): rewrite bio and trajectory from the real CV
chore(db): migrate provider sqlite  postgres for serverless

The conventional commits prefix (feat/fix/content/chore) makes git log --oneline read like a changelog. The repo history tells the same story as this document — because it was written commit by commit.

4. Deploy by git. Push to main → Vercel picks up the commit and runs the build pipeline (prisma generate && prisma db push && seed && next build). No manual steps: the commit is the deploy. Each conversational request was in production in a couple of minutes, under its own SHA.


11

Closing

What began as a spoken brief became three worlds in an afternoon with Claude Design, then an HTML export with React via CDN, and ended up a Next.js site with its own CMS, real content, responsive, and in production under its own domain with Claude Code — all in a conversation, step by step.

Two tools, one same way of working: talk, not syntax. Claude Design filled the space of designing; Claude Code, the space of building. And in the middle, what no tool brings out of the box — the judgment, the what-to-leave-out — stayed mine.

The design system (Ma) was the compass: restraint. The same applied to the code — the simplest responsive layer, the dependency-free markdown render, the honest content. The space between things, here too, was what mattered.

Direction by Luis Venturi — Designed with Claude Design · built with Claude Code · luisventuri.com