Building a CLI-Style Personal Website with Claude

Personal websites are boring. A headshot, a list of jobs, maybe a blog that hasn't been updated in two years. You scroll, you leave, you forget.

I wanted something different. Something that reflects how I actually work - in terminals and chat interfaces. Something interactive.

So I built chrisbrownridge.com as a terminal. You land on a command prompt. You can type questions and chat with Claude about my work, experience, and interests. Or use slash commands for quick info.

Here's how it works.


The Experience

When you visit the site, you see this:

█▀▀ █ █ █▀█ █ █▀▀
█▄▄ █▀█ █▀▄ █ ▄▄█

█▀▄ █▀█ █▀█ █   █ █▄ █ █▀█ █ █▀▄ █▀▀ █▀▀
█▄█ █▀▄ █▄█ ▀▄▀▄▀ █ ▀█ █▀▄ █ █▄▀ █▄█ █▄▄

Founder & Builder

builder. tinkerer. maker.
ask me anything about my work, experience, or interests.

Try these commands:
/contact    Get my email address
/about      Quick bullet points about me
/thoughts   Summary of my recent posts

$ _

The blinking cursor invites interaction. Type a question, get an answer. It feels like talking to someone, not reading a brochure.


Slash Commands

Three built-in commands for common requests:

/contact

Returns my email immediately. No Claude API call needed.

$ /contact
chris@brownridges.com

/about

Quick bullet points about my background:

$ /about
- entrepreneur based in Seattle, originally from the UK
- founder of Cheerful AI, Nuts & Bolts AI, and owner of Flixr
- former CEO at GawkBox, raised $4.4M from Madrona & LVP
- ex-Google, ex-Vungle (acquired by Blackstone)
- trilingual: English, French, German (getting rusty)
- dad of 2 boys, youth soccer coach, skiing & biking enthusiast

/thoughts

This one's dynamic. It calls Claude to summarize my recent LinkedIn posts:

$ /thoughts
Your recent posts focus on building AI-powered tools, particularly
around Claude and automation. Key themes: the importance of
human-in-the-loop patterns, treating AI as a collaborator rather
than a replacement, and the value of shipping fast and iterating.
You've also shared thoughts on founder life and balancing building
with family time.

The /thoughts command demonstrates the Claude integration while providing genuinely useful content.


The Tech Stack

  • Next.js 16 with App Router
  • React 19
  • Tailwind CSS 4
  • Anthropic SDK for Claude integration
  • Gray-matter for markdown parsing (blog posts)

The terminal aesthetic is pure CSS - no images, no special fonts for the ASCII art (just monospace).


The Terminal Component

The core is a React component that manages chat state:

export function Terminal() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (content: string) => {
    // Add user message
    setMessages(prev => [...prev, { role: 'user', content }]);
    setIsLoading(true);

    // Call Claude API
    const response = await fetch('/api/chat', {
      method: 'POST',
      body: JSON.stringify({
        messages: [...messages, { role: 'user', content }]
      })
    });

    const data = await response.json();

    // Add assistant response
    setMessages(prev => [...prev, { role: 'assistant', content: data.response }]);
    setIsLoading(false);
  };

  // Render terminal UI...
}

Commands are handled separately:

const handleCommand = async (command: Command) => {
  setMessages(prev => [...prev, { role: 'user', content: command.name }]);

  if (command.name === '/contact') {
    setMessages(prev => [...prev, {
      role: 'assistant',
      content: 'chris@brownridges.com'
    }]);
    return;
  }

  if (command.name === '/about') {
    setMessages(prev => [...prev, {
      role: 'assistant',
      content: `- entrepreneur based in Seattle...`
    }]);
    return;
  }

  if (command.name === '/thoughts') {
    // This one calls Claude
    const response = await fetch('/api/chat', {
      method: 'POST',
      body: JSON.stringify({
        messages: [{
          role: 'user',
          content: 'Summarize my most recent LinkedIn posts...'
        }]
      })
    });
    // ... handle response
  }
};

The Claude API Route

The backend is a simple Next.js API route:

// app/api/chat/route.ts
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic();

const SYSTEM_PROMPT = `You are an AI assistant on Chris Brownridge's personal website.
You have knowledge about Chris's background, companies, and interests.

Key facts:
- Founder of Cheerful AI (AI-powered email assistant)
- Founder of Nuts & Bolts AI (AI consulting)
- Owner of Flixr (acquired 2022, creative production company)
- Former CEO of GawkBox (raised $4.4M from Madrona, LVP)
- Ex-Google, ex-Vungle (acquired by Blackstone)
- Based in Seattle, originally from UK
- Father of 2 boys, youth soccer coach

Be conversational but concise. This is a terminal interface -
keep responses short and scannable.`;

export async function POST(request: Request) {
  const { messages } = await request.json();

  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 1024,
    system: SYSTEM_PROMPT,
    messages
  });

  return Response.json({
    response: response.content[0].text
  });
}

The system prompt grounds Claude in my specific context. It knows about my companies, background, and communication style.


The macOS Aesthetic

The terminal window has the classic macOS look:

<div className="bg-terminal-bg rounded-xl overflow-hidden shadow-2xl border border-terminal-border/50">
  {/* Window chrome */}
  <div className="flex items-center justify-between px-4 py-3 bg-terminal-input border-b border-terminal-border">
    <div className="flex items-center gap-2">
      <div className="w-3 h-3 rounded-full bg-chrome-red" />
      <div className="w-3 h-3 rounded-full bg-chrome-yellow" />
      <div className="w-3 h-3 rounded-full bg-chrome-green" />
    </div>
    <div className="text-terminal-muted text-sm">
      ~/chrisbrownridge <span className="text-terminal-accent">cli</span>
    </div>
  </div>

  {/* Terminal content */}
  <div className="flex flex-col h-[500px]">
    {/* ... messages and input ... */}
  </div>

  {/* Status bar */}
  <div className="flex items-center justify-end px-4 py-2 bg-terminal-input border-t border-terminal-border">
    <span className="text-terminal-muted">powered by </span>
    <span className="text-terminal-accent">Claude</span>
  </div>
</div>

The color palette uses custom Tailwind tokens:

:root {
  --terminal-bg: #1e1e2e;
  --terminal-input: #2a2a3e;
  --terminal-border: #3a3a4e;
  --terminal-text: #cdd6f4;
  --terminal-muted: #6c7086;
  --terminal-accent: #89b4fa;
  --chrome-red: #f38ba8;
  --chrome-yellow: #f9e2af;
  --chrome-green: #a6e3a1;
}

Typing Animation

New responses animate in character-by-character:

function ChatMessage({ content, animate, onScroll }) {
  const [displayedContent, setDisplayedContent] = useState(animate ? '' : content);

  useEffect(() => {
    if (!animate) return;

    let index = 0;
    const interval = setInterval(() => {
      setDisplayedContent(content.slice(0, index + 1));
      index++;
      onScroll?.();  // Keep scrolled to bottom

      if (index >= content.length) {
        clearInterval(interval);
      }
    }, 15);  // 15ms per character

    return () => clearInterval(interval);
  }, [content, animate, onScroll]);

  return (
    <div className="py-2">
      <span className="text-terminal-accent">$ </span>
      <span className="text-terminal-text whitespace-pre-wrap">{displayedContent}</span>
    </div>
  );
}

The animation only runs on new messages (animate prop), so scrolling through history is instant.


Why This Design?

It reflects how I work

I spend most of my day in terminals and chat interfaces. A traditional website with navigation menus and hero sections feels foreign. This feels like home.

It's interactive

Instead of reading about me, you talk to me (well, an AI trained on my context). Questions get personalized answers. The experience is dynamic, not static.

It's memorable

Everyone has a portfolio with a headshot. Few people have a terminal you can chat with. The novelty creates a lasting impression.

It's practical

The slash commands handle the most common requests instantly. No digging through navigation to find my email. Just /contact and done.


Limitations and Tradeoffs

SEO

Search engines don't chat. The ASCII art and interactive terminal aren't great for traditional SEO. I mitigate this with proper meta tags and a blog section (coming soon) that's crawler-friendly.

Accessibility

Terminal interfaces can be challenging for screen readers. I've added proper ARIA labels and keyboard navigation, but it's not perfect. The commands provide structured fallbacks for key information.

API costs

Every message is a Claude API call. For a personal site with modest traffic, the cost is negligible. At scale, I'd add caching for common questions.


What's Next

A few improvements planned:

  • Blog integration: The /thoughts command pulls from LinkedIn. I want it to also reference actual blog posts on the site.
  • Conversation memory: Currently each page load starts fresh. Persisting conversation history would make it feel more continuous.
  • Richer responses: Code blocks, links, maybe even embedded demos for project discussions.

Try It

Visit chrisbrownridge.com and start typing. Ask about my companies, my background, what I'm working on. See how a personal website can be a conversation instead of a brochure.

And if you build something similar, let me know. I'd love to see more creative takes on the personal website format.


Built with Next.js 16, React 19, Tailwind 4, and Claude. Deployed on Vercel.