App Build Playbook · 2026
Build
Ship. Repeat.
A step-by-step guide to building, branding, and deploying Claude-powered web tools — with the governance, lessons learned, and design standards to do it right.
Next.js + Vercel Anthropic Claude katehaan.dev v1.0 · Living Document

What this is

The goal is to ship real tools that solve real problems — with enough governance that the second one is easier than the first, and the tenth is almost automatic.

This playbook captures every decision, standard, and lesson learned from building Kate's portfolio of Claude-powered tools. It is a living document — updated every time something new is learned.


Standard Tech Stack

Frontend

Next.js 15+ · App Router · TypeScript · Inline styles with CSS variables · No Tailwind color classes

AI & Backend

Anthropic Claude API · claude-sonnet-4-20250514 · Vercel deployment · katehaan.dev subdomains

Fonts

next/font/google — Cormorant Garamond · Outfit · DM Mono. Never import via URL in CSS.

Dev Environment

Cursor + Claude Code · GitHub for version control · Mac (local dev) · Mac mini for agents

Colors, type & patterns

A palette drawn from the land — deep forest greens, warm bone, the amber of late-afternoon light, and the rust of dried terracotta. Sophisticated but alive. Organic without being rustic.


Color Palette

NameHexRole
Pine#1a2421Dark backgrounds, hero sections, nav
Fern#2E4A3ESection labels, borders on dark, card accents
Sage#536B5ESecondary text on dark backgrounds
Bone#FAF8F5Light section backgrounds, primary text on dark
Linen#F0EDE8Card backgrounds on light sections
Canvas#E4DFD8Borders and dividers on light sections
Mustard#C4882APRIMARY ACCENT — all CTAs, labels, highlights
Amber#D4A04AHover states, italic headline accent color
Rust#A84832Warnings, tertiary accent
Ink#221F1AHeadlines on light backgrounds
Mid#4A4238Body text on light backgrounds
Muted#8A8070Metadata, nav links, subdued labels
Role Assignment Rule

Green family (pine, fern, sage) → backgrounds and borders ONLY. Mustard → every accent, CTA, label, and highlight. Never green text on green background.


Typography

Display · Cormorant Garamond

Headlines

Italic, weight 300. Section headings, hero titles, pull quotes. Never body text.

Body · Outfit

Body text

Weight 300 for body, 500-600 for UI labels. Warm and readable.

Labels · DM Mono

SECTION LABELS

0.6rem, letter-spacing 0.2em, uppercase, fern green. Tags, metadata, nav.


Layout Pattern

Hero (above fold)

Dark pine background · Full viewport height · Large Cormorant Garamond italic · Amber accent on key word · Subtle radial glows · Pill tags at bottom

Content sections (below fold)

Bone background · Ink/mid text · Linen cards with canvas borders · Colored left borders · Max-width 860px · Section padding 6rem top/bottom


Key UI Patterns

Quote block:   3px mustard left border · rgba(196,136,42,0.05) bg · italic Cormorant
Cards:         linen bg · canvas border · 3px left border accent
Tags/pills:    DM Mono 0.55rem · canvas border · muted color · borderRadius 2px
Buttons:       DM Mono uppercase · mustard border · transparent bg · no border-radius
Status Live:   fern green border + bg tint
Status In dev: mustard border + bg tint
Section label: DM Mono · 0.6rem · 0.2em tracking · uppercase · fern green
Scroll reveal: IntersectionObserver · opacity 0→1 · translateY(22px)→0 · 0.7s ease

Font Setup in Next.js

Always use next/font/google. Never import Google Fonts via URL in CSS — it causes build errors.

// app/layout.tsx
import { Cormorant_Garamond, Outfit, DM_Mono } from "next/font/google";

const cormorant = Cormorant_Garamond({
  subsets: ["latin"], weight: ["300","400","600"],
  style: ["normal","italic"], variable: "--font-display", display: "swap",
});
const outfit = Outfit({
  subsets: ["latin"], weight: ["300","400","500","600"],
  variable: "--font-body", display: "swap",
});
const dmMono = DM_Mono({
  subsets: ["latin"], weight: ["300","400","500"],
  variable: "--font-mono", display: "swap",
});

// Apply to html element:
<html className={`${cormorant.variable} ${outfit.variable} ${dmMono.variable}`}>
Common Mistake

Never add --font-display: var(--font-display) in globals.css — it creates a circular reference and breaks all fonts. The variables are set automatically by Next.js on the html element.

From idea to deployed

Every app goes through the same five phases. Don't skip ahead — the shortcuts always cost more time than they save.


Concept & Scoping

Write a project brief. Define the problem, vision, audience, must-haves, and open questions. Pick repo name following governance. Don't write code until the brief is clear.

Design System

Copy globals.css from playbook. Set up layout.tsx with next/font/google. Lock in CSS variables before building any UI. Two hours here saves twenty hours later.

Build

Use Cursor + Claude Code. Write files directly — don't paste into terminal. Use cp ~/Downloads/file.tsx to replace files. Commit frequently with proper convention.

BYOK Gate Screen

Create byok branch for public demo. Add GateScreen component. Configure 5 constants. Test with a real Anthropic API key. See BYOK section for full details.

GitHub Setup

Create repo with correct naming. Add .gitignore first. Write README with status badges. Add description and topics. Create GitHub Release at v1.0.

Vercel Deployment

Import repo. Set Framework Preset to Next.js manually — it defaults to Other. Add env vars. Connect custom domain on katehaan.dev. Never share .vercel.app URL.


Phase 4 Checklist — GitHub Setup

  • Repo name follows governance convention
  • .gitignore added on first commit (correct template for repo type)
  • README includes status badge and type badge
  • Description under 100 chars, no period at end
  • Topics added: claude · anthropic · ai · kate-haan + type and domain tags
  • GitHub Release created at v1.0 with release notes
  • Pinned on profile if Tier 1 project

Phase 5 Checklist — Vercel Deployment

  • Framework Preset set to Next.js (not Other)
  • Environment variables added (ANTHROPIC_API_KEY etc.)
  • Custom domain added: [repo-name].katehaan.dev
  • CNAME record added in Squarespace DNS
  • Domain shows green/valid in Vercel
  • README updated with custom domain URL (not .vercel.app)
  • .vercel.app URL never shared publicly

Standards that apply to everything

Decide once, apply everywhere. These standards exist so every new project benefits from every previous lesson.


Repo Naming

TypePatternExamples
OpenClaw Agentsopenclaw-[name]openclaw-heidi, openclaw-kristen, openclaw-sage
OpenClaw Frameworkopenclaw-[descriptor]openclaw-starter, openclaw-playbook
Standalone Toolsdescriptive-kebabescalation-advisor, voice-tone-shifter
Playbooks / Guidesplaybook-[topic]playbook-app-build, playbook-analytics
Portfolio Site[name]-devkatehaan-dev

URL Governance

Lesson Learned

tone-fixer.vercel.app was claimed by someone else before a custom domain was set up. voice-tone-shifter.katehaan.dev is now the canonical URL. Never share .vercel.app links publicly.

PatternUse
[repo-name].katehaan.devAll public-facing tool URLs
[repo-name]-demo.katehaan.devBYOK public demo versions
katehaan.devPortfolio site only

To connect a subdomain: Vercel → Settings → Domains → add subdomain → copy CNAME → Squarespace DNS → Add Record → Type: CNAME, Name: [subdomain], Value: [from Vercel].


Commit Convention

Format: [type]: [what you did] — lowercase, present tense, no period.

TypeWhen to useExample
add:New file, feature, sectionadd: escalation brief output with mustard border
update:Changed existing contentupdate: governance section in playbook
fix:Corrected something brokenfix: byok branch missing Change Key button
remove:Deleted somethingremove: stale weekly meal plan files
docs:README or docs onlydocs: add lessons learned to openclaw-starter
style:Visual only, no logic changestyle: tighten card padding on mobile
deploy:Deployment relateddeploy: trigger vercel redeploy after env var update

Versioning

VersionMeaning
v0.1Live but incomplete — scaffold exists, major sections missing
v1.0MVP — core functionality works, publicly shareable
v1.1, v1.2...Incremental improvements, no breaking changes
v2.0Significant new capabilities or breaking changes

Status Badges

Add to the top of every README. Status badge + type badge.

![Status](https://img.shields.io/badge/status-live-2E4A3E?style=flat-square)
![Status](https://img.shields.io/badge/status-in%20dev-C4882A?style=flat-square)
![Status](https://img.shields.io/badge/status-MVP-536B5E?style=flat-square)
![Type](https://img.shields.io/badge/type-agent-1a2421?style=flat-square)
![Type](https://img.shields.io/badge/type-tool-1a2421?style=flat-square)
![Type](https://img.shields.io/badge/type-playbook-1a2421?style=flat-square)

.gitignore Templates

Agent repos (most restrictive)

google_token.json
*.json
client_secret*.json
.env / .env.*
memory/
meal-planning/
crm/people.md
FUTURE_KATE.md
avatar/
FRANK_FLAG.md
.DS_Store
__pycache__/
*.pyc
node_modules/

App / tool repos

.env
.env.*
.env.local
node_modules/
.next/
out/
dist/
build/
.DS_Store
*.log

Playbook / guide repos

.DS_Store
*.log
node_modules/
drafts/
*.draft.md

Profile Pinning Strategy

SlotRepoRule
1 (permanent)openclaw-starterFramework — most shareable
2 (permanent)escalation-advisorMost polished live tool
3 (permanent)katehaan-devPortfolio site — always current
4 (rotating)Most recently shippedShows momentum
5 (rotating)Most career-relevantReflects current goals
6 (rotating)One personal/charming projectPersonality

Public demo pattern

All public tools use a BYOK (Bring Your Own Key) gate so Claude API costs aren't on Kate. The gate collects the user's Anthropic API key, validates it, and stores it in sessionStorage for the session.


Two versions of every tool: main branch for team use (no gate), byok branch for public demo (gate screen required).

Gate Screen Standard

Source of Truth

voice-tone-shifter.katehaan.dev (formerly tone-fixer — renamed per governance). The GateScreen component lives in gate-screen-starter/ in this repo.

The 5 Constants

Configure these for each app — everything else stays the same.

PRODUCT_NAME     — app name displayed on gate screen
PRODUCT_TAGLINE  — one-line description
FEATURE_LIST     — array of bullet points (what the tool does)
ACCENT_COLOR     — mustard (#C4882A) by default
GITHUB_URL       — link to the public repo

Deployment Flow

  • Build on main branch without gate (team/internal use)
  • Create byok branch from main
  • Add GateScreen component, configure 5 constants
  • Deploy byok branch to [repo-name]-demo.katehaan.dev
  • Deploy main branch to [repo-name].katehaan.dev
  • README links to both versions with clear labels
Key Behavior

The gate screen stores the API key in sessionStorage — it clears when the browser tab closes. Users need to re-enter their key each session. This is intentional for security.

What nobody warns you about

Nobody warns you that building your first app feels like death by a thousand cuts. Not because any single thing is hard — it isn't. It's that every step has a small gotcha attached to it. None of it is insurmountable. All of it is annoying. And most people quit somewhere in that gap.

The gotchas are just tax you pay once.


Install prerequisites before you start building

Node, Git, and a GitHub account. Do it the day before. Don't discover you're missing them mid-build. Install Homebrew first: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" then brew install node git gh.

GitHub doesn't accept passwords anymore

Password authentication was deprecated in 2021. Use the GitHub CLI: brew install gh then gh auth login. Takes five minutes, works cleanly forever.

Never share a .vercel.app URL publicly

tone-fixer.vercel.app was claimed by someone else before a custom domain was set up. Always add [repo-name].katehaan.dev before sharing any link. Subdomains are free and permanent.

Your .gitignore is a security document

Add it before you add anything else. One accidental commit of google_token.json to a public repo means rotating credentials. Use the templates in this playbook. Create on first commit — never the second.

Cursor + Claude Code beats pasting

Paste corruption is real. Backticks, smart quotes, and special characters get mangled when copying from a browser into a terminal. Let Claude Code write directly to the file. For replacements, use cp ~/Downloads/file.tsx ~/project/app/file.tsx.

The design system takes two hours and saves twenty

Lock in CSS variables before building a single page. The temptation is to get something on screen fast and style it later. Later never comes, and you end up with inconsistency that takes longer to fix than the design system would have taken to set up.

You will deploy to a 404 at least once

Check your framework preset in Vercel settings. It defaults to Other when it can't detect the project type. Set it to Next.js manually, then redeploy.

Font variables can be circular

Never write --font-display: var(--font-display) in globals.css. It creates a circular reference and breaks all fonts. Next.js sets these variables automatically on the html element via layout.tsx.

Green-on-green is the silent design killer

The original design system used green for everything — backgrounds, text, accents, borders. Nothing had contrast. Rule: green family = structural (backgrounds, borders) only. Mustard = every single accent. Never mix.

The gap is smaller than it looks

The whole process — zero to a live, custom-domain app with a design system and a BYOK gate — takes one focused session. It wasn't smooth. It also wasn't that hard. The gap between "I could never build that" and "I just did" is mostly just not quitting when the third thing in a row doesn't work.


Every person who has a GitHub they're proud of went through the same frustrations. They just kept going.

Never ship the gray globe

Every site gets a custom favicon. It takes five minutes and immediately signals that someone intentional built this. Use favicon.io/favicon-generator — no design tools required.


The Pattern

SiteTextBackgroundFont Color
katehaan.devKH#1a2421 (pine)#FAF8F5 (bone)
brand-book.katehaan.devKH#1a2421 (pine)#FAF8F5 (bone)
release-playbook.katehaan.dev{}#1a2421 (pine)#C4882A (mustard)
escalation-advisor.katehaan.dev#1a2421 (pine)#C4882A (mustard)
voice-tone-shifter.katehaan.dev~#1a2421 (pine)#C4882A (mustard)
Any new tool1-2 char symbol#1a2421 (pine)#C4882A (mustard)
Any brand propertyKH#1a2421 (pine)#FAF8F5 (bone)
The Rule

Brand properties (portfolio, brand book, playbook) → KH in bone. Tools → short symbol in mustard that hints at what the tool does. Background always pine #1a2421.


How to Generate

  • Go to favicon.io/favicon-generator
  • Set Text to your 1-2 character symbol (see table above)
  • Set Background Color to #1a2421
  • Set Font Color to #FAF8F5 (brand) or #C4882A (tool)
  • Set Font Family — serif for KH, monospace for symbols
  • Set Font Size — 50-60 works well for 2 chars, 80+ for single char
  • Download the zip

How to Install

Plain HTML sites

Drop favicon.ico into the root folder alongside index.html. Add to <head>:

<link rel="icon" href="/favicon.ico">
<link rel="icon" type="image/png"
  sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png"
  sizes="16x16" href="/favicon-16x16.png">

Next.js sites

Drop favicon.ico directly into the app/ folder. Next.js picks it up automatically — no code changes needed.

Don't Forget

Browsers cache favicons aggressively. After updating, hard refresh with Cmd + Shift + R to see the new one. Or open in an incognito window.