Skip to main content
Back to Blog
Building a Zero-Database Portfolio with Static Site Generation
technicalFeatured

Building a Zero-Database Portfolio with Static Site Generation

8 min read

How I built a fully-featured portfolio website without a runtime database using Next.js Static Site Generation, MDX for content, and Git as the single source of truth.

Introduction

When planning my portfolio rebuild, I had a controversial requirement: no runtime database. No PostgreSQL, no MongoDB, not even SQLite. All content would live in Git-tracked files and be generated statically at build time.

This decision led to a architecture that's:

  • Blazing fast: Sub-1s page loads with no database queries
  • Zero-cost: No database hosting or maintenance
  • Version-controlled: Every content change tracked in Git
  • Portable: Deploy anywhere static files are served

Here's how I built it and the tradeoffs I encountered.

The Architecture

┌─────────────────────────────────────────────────────────┐
│                    Git Repository                        │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │ content/blog/│  │content/      │  │lib/          │  │
│  │  *.mdx       │  │projects/     │  │constants.ts  │  │
│  │              │  │  *.json      │  │              │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
└────────────────┬────────────────────────────────────────┘
                 │
                 ▼
         ┌───────────────┐
         │ Build Process │
         │  (Next.js)    │
         └───────┬───────┘
                 │
    ┌────────────┴────────────┐
    │                         │
    ▼                         ▼
┌────────────┐        ┌──────────────┐
│ Static HTML│        │ Static JSON  │
│   Pages    │        │   (Search)   │
└────────────┘        └──────────────┘
    │                         │
    └────────────┬────────────┘
                 │
                 ▼
         ┌───────────────┐
         │   Vercel CDN  │
         │ (Edge Network)│
         └───────────────┘

Every page is pre-rendered at build time. No database queries at runtime. Ever.

Content Management: MDX + JSON

Blog Posts as MDX Files

MDX combines Markdown with React components:

---
title: "My Blog Post"
description: "A great article"
publishedAt: "2025-10-12"
category: "technical"
tags: ["nextjs", "react"]
---
 
## Introduction
 
Regular markdown content here...
 
{/* But you can embed React components! */}
<YouTubeEmbed videoId="dQw4w9WgXcQ" />
 
## Code Examples
 
```typescript
// Syntax highlighting works out of the box
const greeting: string = "Hello, world!";
console.log(greeting);

Math Equations

You can even use KaTeX for math: $E = mc^2$


### Projects as Structured JSON

```json
{
  "id": "portfolio-website",
  "title": "Personal Portfolio Website",
  "description": "A fast, accessible portfolio built with Next.js 15",
  "technologies": ["Next.js", "React", "TypeScript", "Tailwind CSS"],
  "category": "fullstack",
  "status": "completed",
  "image": "portfolio-screenshot.jpg",
  "demoUrl": "https://me.zealer.in",
  "githubUrl": "https://github.com/mnishanth02/portfolio",
  "featured": true,
  "startDate": "2025-09",
  "endDate": "2025-10",
  "problemStatement": "Create a portfolio that serves both technical recruiters and fitness community members...",
  "technicalApproach": "Used Next.js 15 App Router with Static Site Generation...",
  "challenges": [
    "Achieving 95+ Lighthouse scores with rich interactive features",
    "Building a flexible content system without a CMS"
  ],
  "outcomes": [
    "98+ Lighthouse performance score",
    "Sub-1.5s First Contentful Paint",
    "Zero runtime database costs"
  ],
  "metrics": {
    "usersImpacted": 10000,
    "performanceImprovement": "58%",
    "codeReduction": "40%"
  }
}

Content Processing Pipeline

1. Schema Validation with Zod

Every piece of content is validated at build time:

import { z } from 'zod';
 
export const blogPostSchema = z.object({
  title: z.string().min(10).max(100),
  description: z.string().min(50).max(200),
  publishedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  category: z.enum(['technical', 'fitness', 'lifestyle']),
  tags: z.array(z.string()).min(1).max(10),
  image: z.string().optional(),
  featured: z.boolean().default(false),
});
 
export type BlogPost = z.infer<typeof blogPostSchema>;

If content doesn't match schema, build fails - catching errors early.

2. MDX Processing with Content Collections

I use @content-collections/next for type-safe content management:

// content-collections.config.ts
import { defineCollection, defineConfig } from '@content-collections/core';
import { compileMDX } from '@content-collections/mdx';
 
const posts = defineCollection({
  name: 'posts',
  directory: 'content/blog',
  include: '*.mdx',
  schema: (z) => ({
    title: z.string(),
    description: z.string(),
    publishedAt: z.string(),
    category: z.enum(['technical', 'fitness', 'lifestyle']),
    tags: z.array(z.string()),
  }),
  transform: async (document, context) => {
    const mdx = await compileMDX(context, document);
 
    return {
      ...document,
      mdx,
      slug: document._meta.fileName.replace(/\.mdx$/, ''),
      readingTime: calculateReadingTime(document.content),
    };
  },
});
 
export default defineConfig({
  collections: [posts],
});

3. Static Generation with generateStaticParams

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getAllPosts();
 
  return posts.map((post) => ({
    slug: post.slug,
  }));
}
 
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
 
  if (!post) {
    notFound();
  }
 
  return (
    <article>
      <h1>{post.title}</h1>
      <MDXRenderer source={post.mdx} />
    </article>
  );
}

At build time, Next.js generates HTML for every blog post.

Search Without a Database

Client-side search with Fuse.js:

'use client';
 
import Fuse from 'fuse.js';
import { useState, useMemo } from 'react';
 
export function SearchBar({ posts }: { posts: BlogPost[] }) {
  const [query, setQuery] = useState('');
 
  // Create search index
  const fuse = useMemo(() => {
    return new Fuse(posts, {
      keys: ['title', 'description', 'tags'],
      threshold: 0.3, // Fuzzy matching
      includeScore: true,
    });
  }, [posts]);
 
  const results = query
    ? fuse.search(query).map(result => result.item)
    : posts;
 
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search posts..."
      />
 
      {results.map(post => (
        <BlogCard key={post.slug} post={post} />
      ))}
    </div>
  );
}

Search runs entirely in the browser. No API calls needed.

Dynamic Data: The Contact Form Exception

The one exception to the zero-database rule: the contact form.

// app/api/contact/route.ts
import { Resend } from 'resend';
 
const resend = new Resend(process.env.RESEND_API_KEY);
 
export async function POST(request: Request) {
  const body = await request.json();
  const validatedData = contactSubmissionSchema.parse(body);
 
  // Send email - no database storage
  await resend.emails.send({
    from: 'portfolio@me.zealer.in',
    to: 'nishanth.murugan@gmail.com',
    subject: `Contact: ${validatedData.name}`,
    html: validatedData.message,
  });
 
  return Response.json({ success: true });
}

I use Resend to send emails directly. No storage = no GDPR concerns.

Content Updates: The Git Workflow

Updating Content

# 1. Edit content file
vim content/blog/new-post.mdx
 
# 2. Commit changes
git add content/blog/new-post.mdx
git commit -m "Add new blog post"
 
# 3. Push to GitHub
git push origin main
 
# 4. Vercel automatically rebuilds and deploys
# New content live in ~2 minutes

Preview Changes with Pull Requests

git checkout -b draft/new-feature-post
# Edit content...
git push origin draft/new-feature-post
# Open PR → Vercel creates preview deployment
# Review → Merge → Auto-deploy to production

Performance Benefits

Lighthouse Scores: 98-99

No database means:

  • Zero query latency
  • No connection pooling
  • No N+1 queries
  • No database timeouts

Result: Consistent sub-1s page loads

Hosting Costs: $0

With static files:

  • No database server
  • No connection pooling
  • No backup infrastructure
  • Vercel Hobby plan: Free for personal projects

Scalability: Infinite

Static files scale infinitely via CDN:

  • 10 users? Fast.
  • 10,000 users? Still fast.
  • 10 million users? Still fast.

CDN edge caching handles any traffic spike.

Tradeoffs: What You Lose

1. No Real-Time Updates

Content updates require rebuilds:

  • Edit time: Instant
  • Build time: 2-3 minutes
  • Deploy time: 30 seconds
  • Total: ~3 minutes from edit to live

For a portfolio, this is acceptable. For real-time apps, it's not.

2. No User-Generated Content

Users can't directly create content:

  • ❌ Comments
  • ❌ Likes/reactions
  • ❌ User profiles
  • ✅ Contact form (via API route)

Solution: Use third-party services (Disqus for comments, etc.)

3. No Server-Side Personalization

Can't personalize content per user:

  • ❌ "Recommended for you"
  • ❌ A/B testing (server-side)
  • ✅ Client-side personalization (with local storage)

4. Large Sites = Slow Builds

Build time scales with content:

  • 10 posts: ~30 seconds
  • 100 posts: ~2 minutes
  • 1,000 posts: ~15 minutes

Solution: Incremental Static Regeneration (ISR) for large sites

When to Use This Architecture

Perfect For

✅ Portfolios ✅ Blogs ✅ Marketing sites ✅ Documentation ✅ Landing pages ✅ Project showcases

Not Suitable For

❌ Social networks ❌ E-commerce (with inventory) ❌ Real-time dashboards ❌ User-generated content platforms ❌ Apps requiring personalization

Alternative Architectures

If you need more dynamic features:

Hybrid: Static + Database

// Static site with dynamic features
export default async function Page() {
  // Static content
  const post = await getStaticPost();
 
  return (
    <div>
      <h1>{post.title}</h1>
 
      {/* Dynamic comments from database */}
      <Comments postId={post.id} />
    </div>
  );
}

Incremental Static Regeneration (ISR)

export const revalidate = 3600; // Rebuild every hour
 
export default async function Page() {
  const data = await fetch('https://api.example.com/data');
  return <div>{data.title}</div>;
}

Lessons Learned

After 3 months running this architecture:

  1. Simplicity is powerful: No database = fewer moving parts = fewer bugs
  2. Git is underrated as a CMS: Version control, branching, pull requests - all free
  3. Build times matter: Keep content organized for fast builds
  4. Edge cases exist: Some features genuinely need a database

Conclusion

A zero-database architecture isn't right for every project, but for portfolios and content-heavy sites, it's incredibly powerful:

  • Fast: No query latency
  • Cheap: Zero hosting costs
  • Reliable: No database downtime
  • Simple: Fewer components to maintain

The key is understanding the tradeoffs and choosing the right tool for the job.

For my portfolio, it was the perfect choice.

Resources

Tags

nextjsstatic-sitemdxarchitecturejamstack