Back to Blog
development
8 min read

From Zero to Deployed: Building a Professional Website with Next.js and Tailwind

step-by-step breakdown of building webceive.com: component architecture, shadcn/ui integration, dark mode, and deployment. includes real code examples and lessons learned...

The Webceive Team

Systems Integration & Automation Experts

From Zero to Deployed: Building a Professional Website with Next.js and Tailwind

Building webceive.com was more than just creating a website - it was about establishing credibility, demonstrating technical expertise, and creating a foundation for our business. Here's the complete breakdown of how we went from blank screen to production deployment.

The Requirements

Before writing any code, we defined what success looked like:

  • Professional appearance that builds trust
  • Mobile-first responsive design
  • Sub-1-second load times
  • Dark/light mode support
  • Contact form with validation
  • Blog system for content marketing
  • SEO optimization
  • Accessibility compliance

The Tech Stack Decision

Framework: Next.js 15

  • App Router for modern routing
  • Built-in optimization (images, fonts, etc.)
  • Static export for CDN deployment
  • Server-side rendering for SEO

Styling: Tailwind CSS + shadcn/ui

  • Utility-first CSS for rapid development
  • shadcn/ui for consistent component library
  • Custom design system with CSS variables
  • Dark mode support out of the box

Deployment: Docker + nginx

  • Self-hosted for complete control
  • Static export served by nginx
  • SSL termination and caching
  • Zero vendor lock-in

Project Architecture

src/
├── app/                 # Next.js App Router
│   ├── globals.css     # Global styles and CSS variables
│   ├── layout.tsx      # Root layout with providers
│   ├── page.tsx        # Homepage
│   ├── blog/           # Blog system
│   └── contact/        # Contact page
├── components/         # Reusable components
│   ├── ui/            # shadcn/ui components
│   ├── navbar.tsx     # Navigation
│   └── contact-form.tsx # Contact form
├── data/              # Static content
└── lib/               # Utilities

Component Development Strategy

1. Design System First

We started with a cohesive design system in globals.css:

:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --card: 0 0% 100%;
  --card-foreground: 222.2 84% 4.9%;
  --popover: 0 0% 100%;
  --popover-foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  /* ... more CSS variables ... */
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  /* ... dark mode overrides ... */
}

This approach gave us consistent theming across all components.

2. Component Library with shadcn/ui

Instead of building UI components from scratch, we used shadcn/ui:

npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add input
npx shadcn@latest add form

Each component comes with:

  • TypeScript definitions
  • Accessibility features
  • Customizable styling
  • Dark mode support

3. Mobile-First Navigation

The navigation component handles both desktop and mobile:

export default function Navbar() {
  return (
    <header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/80 backdrop-blur-xl">
      <div className="container flex h-16 items-center justify-between">
        <Logo />

        {/* Desktop Navigation */}
        <nav className="hidden md:flex items-center space-x-8">
          <NavLinks />
        </nav>

        {/* Mobile Navigation */}
        <Sheet>
          <SheetTrigger asChild className="md:hidden">
            <Button variant="ghost" size="sm">
              <Menu className="h-5 w-5" />
            </Button>
          </SheetTrigger>
          <SheetContent>
            <MobileNavLinks />
          </SheetContent>
        </Sheet>

        <ThemeToggle />
      </div>
    </header>
  );
}

Key features:

  • Sticky positioning with backdrop blur
  • Responsive design (hidden/shown based on screen size)
  • Theme toggle for dark/light mode
  • Mobile-friendly slide-out menu

Form Development with Validation

The contact form needed to be bulletproof. We used react-hook-form with Zod validation:

const formSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Please enter a valid email address"),
  company: z.string().optional(),
  phone: z.string().optional(),
  project: z.string().min(10, "Please tell us more about your project"),
});

export default function ContactForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: "",
      email: "",
      company: "",
      phone: "",
      project: "",
    },
  });

  const onSubmit = async (values: z.infer<typeof formSchema>) => {
    try {
      // Form submission logic
      console.log(values);
    } catch (error) {
      console.error("Form submission error:", error);
    }
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        {/* Form fields with proper validation and error handling */}
      </form>
    </Form>
  );
}

This approach provided:

  • Client-side validation with instant feedback
  • Type-safe form handling
  • Consistent error messaging
  • Accessibility compliance

Performance Optimization

1. Static Export Configuration

Next.js configuration for optimal static export:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  trailingSlash: true,
  images: {
    unoptimized: true,
  },
  experimental: {
    optimizeCss: true,
  },
};

module.exports = nextConfig;

2. Image Optimization

All images optimized using Next.js Image component:

import Image from 'next/image';

<Image
  src="/hero-image.jpg"
  alt="Professional website development"
  width={800}
  height={600}
  priority
  className="rounded-lg"
/>

3. Font Optimization

Custom fonts loaded efficiently:

import { Inter, Roboto_Mono } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
});

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
});

Blog System Implementation

1. Dynamic Routing

Created dynamic routes for blog posts:

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = getBlogPost(slug);

  if (!post) {
    notFound();
  }

  return (
    <article className="prose prose-neutral dark:prose-invert max-w-4xl mx-auto">
      <h1>{post.title}</h1>
      <div className="flex items-center gap-4 text-sm text-muted-foreground">
        <time>{post.date}</time>
        <span>{post.readTime}</span>
      </div>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

2. Blog Listing Page

Main blog page with filtering and search:

export default function BlogPage() {
  const [searchQuery, setSearchQuery] = useState("");
  const [selectedCategory, setSelectedCategory] = useState("all");

  const filteredPosts = blogPosts.filter(post => {
    const matchesSearch = post.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
                         post.summary.toLowerCase().includes(searchQuery.toLowerCase());
    const matchesCategory = selectedCategory === "all" || post.category === selectedCategory;

    return matchesSearch && matchesCategory;
  });

  return (
    <div className="container mx-auto px-4 py-8">
      <div className="mb-8">
        <SearchBar value={searchQuery} onChange={setSearchQuery} />
        <CategoryFilter selected={selectedCategory} onChange={setSelectedCategory} />
      </div>

      <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
        {filteredPosts.map(post => (
          <BlogCard key={post.id} post={post} />
        ))}
      </div>
    </div>
  );
}

Testing Strategy

1. Component Testing with Jest

import { render, screen } from '@testing-library/react';
import ContactForm from '../contact-form';

describe('ContactForm', () => {
  it('renders all form fields', () => {
    render(<ContactForm />);

    expect(screen.getByLabelText(/name/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/project/i)).toBeInTheDocument();
  });

  it('shows validation errors for invalid input', async () => {
    render(<ContactForm />);

    const submitButton = screen.getByRole('button', { name: /send message/i });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(screen.getByText(/name must be at least 2 characters/i)).toBeInTheDocument();
    });
  });
});

2. E2E Testing with Cypress

describe('Website Navigation', () => {
  it('should navigate between pages correctly', () => {
    cy.visit('/');

    // Test navigation links
    cy.get('[data-testid="nav-about"]').click();
    cy.url().should('include', '/about');

    // Test contact form
    cy.get('[data-testid="nav-contact"]').click();
    cy.get('[data-testid="contact-form"]').should('be.visible');

    // Test blog
    cy.get('[data-testid="nav-blog"]').click();
    cy.get('[data-testid="blog-posts"]').should('be.visible');
  });
});

Deployment Configuration

1. Docker Setup

FROM nginx:alpine

# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf

# Copy built website
COPY out/ /usr/share/nginx/html/

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost/ || exit 1

EXPOSE 80

2. Build Process

#!/bin/bash
echo "Building webceive.com..."

# Install dependencies
npm ci

# Run tests
npm run test

# Build for production
npm run build

# Create Docker image
docker build -t webceive-website .

# Deploy to production
docker-compose up -d

3. nginx Configuration

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # Enable gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Handle Next.js routing
    location / {
        try_files $uri $uri.html $uri/ =404;
    }

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
}

Performance Results

After optimization:

  • Lighthouse Score: 98/100
  • First Contentful Paint: 0.6s
  • Largest Contentful Paint: 0.9s
  • Time to Interactive: 1.1s
  • Total Bundle Size: 89KB gzipped

Lessons Learned

What Worked Well

  1. shadcn/ui components - Saved weeks of development time
  2. Tailwind CSS - Rapid prototyping and consistent styling
  3. Static export - Lightning-fast load times
  4. Docker deployment - Reproducible and scalable

What We'd Do Differently

  1. Start with testing - Added tests later, should have been TDD
  2. Component documentation - Storybook stories from day one
  3. Performance budget - Set limits earlier in development
  4. Accessibility audit - Should have been part of the build process

The Business Impact

The website became more than just marketing material:

  • Client confidence - Technical competence demonstrated through code quality
  • Cost savings - $15,000 saved vs. agency development
  • Ongoing control - No vendor dependencies for changes
  • Technical credibility - Clients trust us with their technical decisions

Code Repository Structure

Final project structure:

webceive-website/
├── src/                 # Source code
├── public/             # Static assets
├── stories/            # Storybook stories
├── cypress/            # E2E tests
├── docker-compose.yml  # Local development
├── Dockerfile          # Production build
├── nginx.conf          # Web server config
└── package.json        # Dependencies and scripts

Next Steps

With the foundation complete, we're now working on:

  • Advanced analytics and conversion tracking
  • A/B testing framework for optimization
  • Automated content generation for the blog
  • Progressive Web App features
  • Advanced SEO optimization

The website is live at webceive.com - a testament to what's possible with modern web technologies and attention to detail.


Next: Self-hosting vs SaaS: Our Infrastructure Decisions for a Bootstrapped Startup

Written by The Webceive Team

Systems Thinking for Growing Businesses

Share:

Ready to Transform Your Business?

Get a free systems assessment and discover how integrated automation can streamline your workflows and scale your operations.