Skip to main content

Building This Website: A Human-AI Collaboration Story

17 min readWritten by a human, edited by AI
AINext.jsDevelopmentDesign

"I should build a website."

That was the thought I had randomly when testing out new IDEs (Antigravity), different LLMs, and trawling through my Youtube algorithm on recommended ways to use GenAI. But I didn't just want this to be another generic website obviously vibe coded and looking like every other Tailwind site out there, I wanted to be deliberate and purposeful in not only the technical choices and the design decisions but also the way in which I interacted with the code and my LLM of choice. I wanted to understand whether I could avoid the usual 15 files changes and thousands of lines of code that normally occurs when you one shot a prompt and lose track of what tech you're using, why you're using it, and how that supports the outcomes you are aiming for. I am a strong believer that GenAI will support faster outcomes, but I am also a believer that to build something that is secure, resilient, scalable, accessible, and easy to iterate on you need an engineer to consider all of those things up front and steer the tooling to deliver this in the right way. This website didn't emerge from a template or a weekend hackathon. It was built through a deliberate, methodical collaboration between human vision and AI capability - an experiment in what's possible when you treat AI development as true pair programming rather than just autocomplete.

What We Set Out To Build

The goal was clear from the start: create a personal website that serves as a professional presence, portfolio, and blog platform. But more importantly, it needed to be uniquely mine - not another generic Tailwind template that looks like every other developer site.

But beyond the technical requirements, I wanted to learn something deeper: How do you build software collaboratively with AI in a way that teaches you, not just delivers code? My initial prompt wasn't "I want to build a website" it was a request that this was a collaborative piece in which the first step was to create some guideline files that we would adhere to throughout the project. Those files are available in the ai-guidelines section of the Github repo if you wanted to see what we produced, covering project vision, architecture, design system, content guidelines, and development practices.

Core Requirements:

  • Typography-first design with bold, impactful type
  • Custom design system (no default Tailwind aesthetic)
  • MDX-based content management rather than integrating a 3rd party platform
  • Multiple content types (blog posts, projects, labs, books, bookmarks)
  • Accessibility and SEO as first-class concerns
  • Modern Next.js patterns

Learning: Map out what you are trying to achieve before writing any code. What are your fundamentals? What are your constraints? Don't stop being an engineer just because you're now writing less of the code.

A Different Approach to AI Development

Now that we had the plan we could start on the implentation. Most AI-assisted development I've seen falls into two camps: either a vibe coded black box or a hyper-critical write and review approach which diminishes any potential productivity gains. I've found that vibe coding works when building out something super MVP, like an app idea or a simple tool to automate a workflow - I've used it and it's great in certain use cases. On the flip side, I've seen development workflows grind to a crawl and frustrations grow when the code produced is not what was expected or wanted. Coding guidelines are ignored, code is overly verbose, and files are changed all over the codebase making it massively difficult to then comprehend and correct where necessary. Any engineer will tell you that massive PRs are a headache and the rate of production of code and file changes by LLM is like that on steroids. We deliberately chose a third path.

Plan Before Action

Every feature started with discussion, not implementation. I would explain what I wanted to achieve in as much detail as I could think of and then ask what other things would need to be considered before being able to implement. The AI would present options with trade-offs. As an example, I was unsure about the use of a third-party CMS solution didn't want the overhead of having to maintain an integrated solution, with authentication, etc. I suggested using MD files which could be committed to the codebase and asked about the use of that within the context of our application, in accordance with the guidelines we had created. The response was solid, giving me options, a recommendation, and allowing me to probe deeper:

If you don't want to use a CMS and are comfortable with MD syntax, you might want to consider using MDX as we have built this application using React (JSX). Here are soem options for us:
1. Basic MD for simple compilation (fastest, no extras)
2. MDX With syntax highlighting and TOC extraction (better UX, more setup)
3. Full CMS-like solution with contentlayer (most features, added complexity)

What are your thoughts?

We'd often go back and forth discussing options in more detail and only after agreeing on the approach did we implement. This up front work took a little time but not any longer than I would have spent doing my own research. I certainly wouldn't have jumped to MDX files and to have that as a suggestion and, critically, to have an expert in the LLM who knows how to implement them saved me a massive headache and enormous amounts of time down the road.

Learning: You have all the expertise in the world available at your fingertips in a more accessible way than you ever have before. Lean into that knowledgbase and ask for opinions and request agremeent before implementation.

Small, Focused Iterations

Rather than building entire features in one go, we worked in small chunks.

  1. Create basic component structure → test → refine
  2. Add styling → verify against design system → adjust
  3. Add interactivity → check accessibility → improve

Each step was reviewable, testable, and understandable. No 15-file commits that are impossible to reason about. This isn't "best practices for working with GenAI" this is just good engineering practice.

When we're building things as engineers, we'll break things down into manageable chunks, to help contain context and understanding and build in a way that helps us verify that we've solved the solution. It also gives reviewers less of a headache as they try to gain the context of the changes made so that they can feedback effecively. What we're doing here is good engineering practice and not boiling the ocean

Comprehensive Documentation

As we made decisions, we documented them. The docs/ai-guidelines/ directory became our shared source of truth, containing:

  • Project vision and goals - What we're building and why
  • Design system - Every colour, font, spacing token documented
  • Development practices - How we work together, what quality means
  • Architecture guidelines - Patterns to follow, anti-patterns to avoid

This documentation wasn't just for the AI—it clarified my own thinking and served as a forcing function for intentional decisions.

Technology Choices

Next.js 15 with App Router

We went all-in on Next.js 15's App Router and Server Components. Not because it's trendy, but because it aligned with our goals:

  • Server Components by default - Only 7 client components in the entire site
  • Static generation - All content pages are pre-rendered at build time
  • Zero JavaScript for content - Blog posts render with no client-side JS
  • File-based routing - Intuitive organisation that maps to site structure

Result: First load JS under 120KB across all pages. The blog listing page? Just 109KB total, with only 175 bytes of page-specific JavaScript.

TypeScript in Strict Mode

No compromises here. TypeScript strict mode enabled, zero any types, full type safety throughout. We used Zod for runtime validation of MDX frontmatter, giving us both compile-time and runtime type safety.

The win: Zero TypeScript errors. Not "mostly clean with a few anys"—actually zero errors.

Custom Tailwind Design System

This was crucial. We wanted to avoid the "generic Tailwind look" that plagues so many sites. So we:

  1. Defined custom design tokens - No blue-500 or gray-600 anywhere
  2. Typography-first approach - Playfair Display for impact, DM Sans for readability
  3. Vivid accents on muted base - Sophisticated neutrals with strategic pops of colour
  4. Documented everything - Every colour has a name and purpose

Example from our design system:

colors: {
  primary: '#0066FF',      // Vibrant blue for trust
  secondary: '#FF3366',    // Coral for energy
  accent: '#6B4FFF',       // Purple for creativity
  'text-primary': '#1A1A1A',
  'text-secondary': '#6B6B66',
  background: '#FAFAF8',   // Warm off-white
}

No more "is this gray-700 or gray-800?"—every colour has semantic meaning.

MDX for Content

We chose MDX for blog posts, projects, labs, and books because it gives us:

  • Version control - All content lives in Git
  • Rich components - Embed interactive elements in posts
  • Type-safe frontmatter - Zod validates every piece of metadata
  • Zero database - Keep it simple, deploy to Vercel

The tradeoff? No visual CMS. But for a personal site, the ability to write in my favorite editor and track changes in Git is worth it.

The Development Process

Branch-Based Workflow with CI/CD

Even for a personal project, we set up a team-like workflow:

  • Feature branches for every change
  • Pull requests with automated checks
  • CI/CD pipeline running TypeScript, linting, and builds
  • Preview deployments for every branch (via Vercel)
  • Merge to main triggers production deploy

Why this overhead for a solo project? Two reasons:

  1. Learning - Understanding modern team workflows firsthand
  2. Quality gates - Can't merge broken code, even if you're working alone

The CI pipeline caught numerous issues before they reached production. Worth every minute of setup time.

Testing with Playwright MCP

One of our experiments was using the Playwright MCP (Model Context Protocol) for visual testing and accessibility validation. The idea: the AI could programmatically test designs, check responsive behaviour, and report accessibility issues.

What worked:

  • Automated accessibility checks caught missing ARIA labels
  • Responsive testing across breakpoints was faster than manual checking
  • Screenshot-based visual verification for complex components

What didn't:

  • Setup complexity was high for the value gained
  • False positives required manual review anyway
  • Slowed down the development loop
  • Better for larger teams than solo projects

Lesson learned: Playwright is powerful, but for a small site, manual testing in dev tools was often faster. We eventually relied more on Lighthouse audits and manual accessibility testing.

Vercel MCP for Deployments

The Vercel MCP integration, however, was fantastic. Being able to:

  • Trigger deployments programmatically
  • Check build status and logs
  • Monitor performance metrics
  • Access deployment URLs for testing

This actually saved time and caught deployment issues early.

The Audits

About three-quarters through development, we paused to audit everything we'd built. Not surface-level checks—deep, comprehensive audits.

Accessibility Audit

Initial score: 70% WCAG 2.1 AA compliance

Issues found:

  • Missing skip links for keyboard navigation
  • Form labels not properly associated
  • Some decorative SVGs lacking aria-hidden
  • No prefers-reduced-motion support
  • Interactive elements missing ARIA labels

We fixed them all. Final score: 92% WCAG AA compliance, including Level AAA motion support.

Critical fixes included:

// Skip to main content link
<a 
  href="#main-content" 
  className="sr-only focus:not-sr-only"
>
  Skip to main content
</a>
 
// Proper ARIA labels on filter buttons
<button
  aria-label={`Filter by ${type}`}
  aria-pressed={selectedType === type}
>
  {label}
</button>
 
// Live regions for dynamic content
<div 
  role="status" 
  aria-live="polite" 
  aria-atomic="true"
  className="sr-only"
>
  {filteredCount} items found
</div>

SEO Audit

Initial score: 65/100

Critical missing pieces:

  • No robots.txt
  • No XML sitemap
  • Missing Open Graph tags
  • No structured data (JSON-LD)
  • Missing canonical URLs

We implemented comprehensive SEO:

// Dynamic sitemap with all content types
export default function sitemap(): MetadataRoute.Sitemap {
  return [
    ...staticPages,
    ...blogPosts.map(post => ({
      url: `${baseUrl}/blog/${post.slug}`,
      lastModified: new Date(post.frontmatter.publishedAt),
      changeFrequency: 'monthly',
      priority: 0.6,
    })),
    // ... projects, labs, books
  ]
}
 
// Structured data for blog posts
const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: post.frontmatter.title,
  description: post.frontmatter.description,
  author: {
    '@type': 'Person',
    name: 'Matt Asbury',
  },
  datePublished: post.frontmatter.publishedAt,
  // ... more schema.org fields
}

Final score: 94/100 with comprehensive metadata, Open Graph tags, Twitter Cards, and structured data throughout.

Engineering Audit

The code quality audit was enlightening:

Strengths:

  • Zero TypeScript errors (strict mode)
  • Excellent component organisation
  • Server-first architecture (28 Server Components, only 7 Client Components)
  • Minimal technical debt
  • Lean dependencies (12 production packages)

Issues found:

  • Duplicate MDX compilation logic across 4 files
  • No error boundaries
  • No custom 404 page
  • Missing loading states

We fixed the critical issues (error boundaries, 404 page) and documented the minor technical debt for future cleanup.

Final engineering grade: A+ (95/100)

Technical Challenges and Wins

Challenge: MDX with Next.js 15

Next.js 15's async params pattern was brand new when we built this. The documentation was sparse, and patterns weren't established.

// New async params pattern
export default async function BlogPostPage({ 
  params 
}: { 
  params: Promise<{ slug: string }> 
}) {
  const { slug } = await params  // Must await!
  // ...
}

We had to figure out type-safe patterns for this while also handling MDX compilation, Zod validation, and rehype plugins.

Win: We now have a robust, reusable pattern that works across blog, projects, labs, and books.

Challenge: Custom Design System in Tailwind

Moving away from default Tailwind tokens meant we couldn't rely on muscle memory or common patterns. Every colour, every spacing decision required thought.

Win: The site looks unique. You can't immediately tell it's "another Tailwind site." The design system is fully documented, making it easy to maintain consistency.

Challenge: Type-Safe MDX

Getting TypeScript to understand MDX imports while also validating frontmatter at runtime took careful setup:

// Zod schema for type safety
const BlogPostFrontmatterSchema = z.object({
  title: z.string(),
  description: z.string(),
  publishedAt: z.string(),
  tags: z.array(z.string()),
  featured: z.boolean(),
  draft: z.boolean(),
})
 
// TypeScript type inference
type BlogPostFrontmatter = z.infer<typeof BlogPostFrontmatterSchema>
 
// Runtime validation
const frontmatter = BlogPostFrontmatterSchema.parse(rawFrontmatter)

Win: Complete type safety from file system to React components. Invalid frontmatter fails at build time, not runtime.

Challenge: Performance Without Sacrificing Features

We wanted rich features (syntax highlighting, table of contents, image optimization) without bloating the bundle.

Win: Through careful use of Server Components and code splitting:

  • Homepage: 116KB first load JS
  • Blog listing: 109KB first load JS
  • Individual blog posts: 110KB first load JS

That's smaller than many sites with far fewer features.

What Worked Really Well

1. Documentation-First Development

Having comprehensive AI guidelines forced us to make intentional decisions. When we wondered "how should we handle error states?"—we'd document the pattern, then implement it consistently.

2. Quality Gates in CI/CD

Automated checks caught:

  • 7 TypeScript errors before they reached main
  • 12 linting issues that would have been painful to fix later
  • 3 build failures from missing dependencies

The overhead of setting up GitHub Actions paid for itself within the first week.

3. Design System Documentation

Every time we added a component, we checked against the design system docs. This prevented drift and kept the aesthetic consistent across 30+ components.

4. Audit-Driven Improvements

The accessibility, SEO, and engineering audits provided a roadmap. Rather than randomly improving things, we had prioritized, actionable issues with clear success criteria.

What We'd Do Differently

Start Audits Earlier

We waited until most features were built to audit. Starting audits earlier would have caught patterns before they were duplicated across files.

Simpler Testing Strategy

The Playwright MCP experiment was interesting but ultimately too complex for this project's needs. Simpler manual testing with Lighthouse audits would have been faster.

Extract Common Code Sooner

We noticed the MDX compilation duplication but kept deferring the fix. Now it's in 4 files. Extracting it to a shared utility on the second duplication would have been better.

More Content Upfront

We built the blog system with 3 sample posts. Writing more real content earlier would have surfaced UX issues (reading experience, navigation patterns) sooner.

Lessons About Human-AI Collaboration

AI Excels At...

  1. Boilerplate and setup - Configuring Next.js, TypeScript, Tailwind, ESLint
  2. Following documented patterns - Once we defined patterns, AI applied them consistently
  3. Comprehensive audits - Checking every component against accessibility and SEO standards
  4. Iterative refinement - "Make this more responsive," "Improve this colour contrast"

Humans Are Essential For...

  1. Vision and taste - What should this feel like? What's the right aesthetic?
  2. Prioritization - Which features matter? What can ship later?
  3. Trade-off decisions - Is this complexity worth the benefit?
  4. Final review - Does this actually work well in practice?

The Sweet Spot

The magic happened when we combined AI's systematic thoroughness with human judgement:

  • AI: "This button needs aria-label for screen readers"

  • Human: "Yes, and let's also add aria-pressed to show toggle state"

  • AI: "Here's a blog post card component following the design system"

  • Human: "Perfect, now let's adjust the hierarchy—the date should be more subtle"

  • AI: "TypeScript error in line 47: Type 'string | undefined' is not assignable..."

  • Human: "Ah, we need to handle the case where publishedAt might be missing. Let's add Zod validation."

The Result

After hundreds of commits across dozens of feature branches, we have:

  • A unique, custom-designed personal website (not a template)
  • 92% WCAG 2.1 AA accessibility compliance (some sites never reach this)
  • 94/100 SEO score (comprehensive metadata and structured data)
  • Zero TypeScript errors (strict mode, no any types)
  • ~110KB average page weight (smaller than many marketing sites)
  • Comprehensive documentation (design system, development practices, architecture)

More importantly, I understand every line of code. I can explain why we chose this pattern over that one. I learned modern Next.js deeply, not superficially.

For Other Developers

If you're building something with AI assistance, consider:

Do:

  • Treat it like pair programming, not autocomplete
  • Document your decisions and patterns
  • Plan before implementing
  • Iterate in small, reviewable chunks
  • Run comprehensive audits (accessibility, SEO, performance)
  • Set up quality gates (CI/CD, TypeScript strict mode)

Don't:

  • Accept code you don't understand
  • Skip the planning phase
  • Delegate high-level architecture decisions
  • Merge unreviewed code
  • Assume AI suggestions are always optimal
  • Skip accessibility and performance considerations

Remember: The goal isn't to build faster—it's to build better while learning deeply. AI is a tool that amplifies your capabilities, not a replacement for thinking.

What's Next

Now that the foundation is solid, the focus shifts to content:

  • Regular blog posts (this is the first of many)
  • Project write-ups and case studies
  • Documented learnings and experiments
  • Continuous refinement based on usage

The site is built to evolve. Adding new content types is straightforward thanks to the patterns we established. The design system is documented and extensible. The architecture is clean and maintainable.

Most importantly, I have a place I'm genuinely excited to share and continue building.

Try It Yourself

All the code and documentation for this site is in a private repository, but the patterns and practices we used are applicable to any project:

  1. Define your design system before building components
  2. Document your patterns as you discover them
  3. Audit comprehensively (accessibility, SEO, performance, code quality)
  4. Use TypeScript strictly for confidence in refactoring
  5. Treat AI as a collaborator who needs clear communication

Building with AI doesn't mean building fast and sloppy. It means building thoughtfully with more systematic thoroughness than you'd have time for alone.

The result is software you're proud of—and code you actually understand.


Questions about the process? Thoughts on human-AI collaboration in development? I'd love to hear your experiences.

Share: