Vibe Coding a Portfolio My Way

October 14, 20255 min

Ten hours from start to production. A site that's fast to load, fast to update, and fast to extend. Here's how I got there, and what it uses.

The Stack in Practice:

Frontend: Astro (SSR)

API: Hono (Node.js)

CMS: Directus

Database: PostgreSQL

Infrastructure: Docker Compose

Deployment: Cloudflare Tunnels

Not arbitrary. Each choice paid dividends.

What Astro Actually Delivered

Shipped: 12KB JavaScript (total)

v0.dev's Next.js template: ~200KB JavaScript minimum. Just for the framework.

Astro conversion: 12KB total. Most pages are pure HTML.

What this means:

  • First Contentful Paint: <0.8s
  • Time to Interactive: <1.2s
  • Lighthouse score: 97

The trade: Lost React interactivity. Gained speed.

Was it worth it? For a portfolio? Absolutely. I don't need client-side state management for showing projects.

Build Time: 8 Seconds

Next.js dev server startup: 15-20 seconds. Every change: 2-3 seconds hot reload.

Astro: 8 second full build. Changes: instant.

What this means during development:

  • Change a component → See it immediately
  • No waiting for webpack rebuilds
  • Tight feedback loop = faster iteration

The multiplier: 20 small changes in 10 minutes vs 20 small changes in 30 minutes.

Content Routes: Zero Configuration

Needed routes for /blog/[slug] and /work/[slug].

Next.js approach:

  • Set up dynamic routes
  • Configure getStaticPaths
  • Handle ISR if you want updates
  • Deploy to regenerate

Astro approach:

// pages/blog/[slug].astro
const { slug } = Astro.params
const post = await getBlogPost(slug)

That's it. SSR by default. No build required for content updates.

What this delivered: Update a post in Directus → Refresh → See it. No deployment.

What Directus Actually Delivered

Non-Developer Content Updates

My partner can now:

  • Add a new work project (no code)
  • Update project descriptions (no code)
  • Reorder featured projects (drag and drop)
  • Fix typos in my bio (instant)

Before (hardcoded content): Every change = text me → I edit code → commit → deploy → 10 minutes.

Now: Every change = edit in Directus UI → save → instant.

The unlock: I'm not the bottleneck anymore.

Real Relational Data

Work projects have:

  • Technologies (many-to-many)
  • Categories (employment, hackathon, research)
  • Status (published, draft, archived)

Strapi/Payload: Would work, but Directus's relational UI is better.

What this means: Filtering "show me all hackathon projects using TypeScript" is a UI dropdown, not a query I need to write.

TypeScript SDK Out of the Box

import { createDirectus, rest, readItems } from '@directus/sdk'

const directus = createDirectus<Schema>('http://directus:8055').with(rest())

// Fully typed
const projects = await directus.request(
  readItems('work_projects', {
    filter: { status: { _eq: 'published' } },
    sort: ['order']
  })
)

What this delivered:

  • Autocomplete for all fields
  • Type errors if I misspell a field name
  • Build fails before runtime errors

The save: Caught 8 bugs during development that would have been runtime errors in production.

What Hono Actually Delivered

200KB → 12KB API Layer

Express is 200KB+. Hono is 12KB.

Does it matter? For cold starts: yes.

Reality check: This API runs in Docker on my homelab. Cold starts aren't an issue. I chose Hono for DX, not bundle size.

Type-Safe Routing

// api/src/index.ts
app.get('/api/work-projects/:slug', async (c) => {
  const slug = c.req.param('slug')  // TypeScript knows this is a string
  const project = await getWorkProject(slug)
  return c.json({ data: project })  // TypeScript validates the response
})

Express equivalent: Type safety is manual. You add types yourself or use separate libraries.

What this delivered: Zero "undefined is not a function" errors. The API layer just works.

Edge-Ready (Future Proof)

I'm running this on Docker now. If I ever move to Cloudflare Workers or Deno Deploy, Hono already runs there.

Express: Tied to Node.js.

Hono: Platform-agnostic.

The bet: Web runtimes are converging. Hono works everywhere.

What Docker Compose Actually Delivered

Identical Dev/Prod

My laptop: docker compose up My homelab server: docker compose up

Same configs. Same behavior. No surprises.

What this prevented:

  • "Works on my machine" → It works everywhere
  • Environment variable mismatches → All in .env
  • Service startup races → Healthchecks enforce order

The save: Zero deployment debugging. If it works locally, it works in production.

One Command Setup

New machine? Clone repo. Run:

docker compose up

That's it. Frontend, API, Directus, PostgreSQL all running. Connected. Networked. Ready.

Without Docker: Install Node. Install PostgreSQL. Configure PostgreSQL. Set up networking. Configure environment variables. Debug why they can't talk to each other.

With Docker: One command.

Time saved: 2 hours of setup → 2 minutes.

What Cloudflare Tunnels Actually Delivered

Zero Network Configuration

Traditional self-hosting requirements:

  • Port forwarding on router
  • Dynamic DNS for changing IP
  • SSL certificate management
  • Firewall rules
  • DDoS protection (hope for the best)

Cloudflare Tunnel:

cloudflared tunnel --url http://localhost:4321

That's it. HTTPS included. DDoS protection included. No exposed ports.

What this means: I can self-host on residential internet without telling my ISP or configuring my router.

Free HTTPS

Let's Encrypt works, but you need to:

  • Run certbot
  • Configure renewal
  • Handle challenges
  • Set up nginx/caddy
  • Debug when it breaks

Cloudflare Tunnel: HTTPS by default. Zero configuration.

Time saved: 4 hours of SSL debugging → 0 minutes.

The Performance Reality

Lighthouse Scores

  • Performance: 97
  • Accessibility: 100
  • Best Practices: 100
  • SEO: 100

What got us here:

  • Astro's minimal JavaScript
  • Optimized image loading
  • Server-side rendering
  • Clean HTML structure

The baseline: v0.dev template scored 85-90. Conversion to Astro added 10 points.

Real User Metrics

First Contentful Paint: 0.6-0.8s Time to Interactive: 1.0-1.2s Largest Contentful Paint: 1.1s

For comparison:

  • Average Next.js site: 2-3s FCP
  • Average portfolio site: 3-4s FCP

Why: No React hydration. No JavaScript bundles. Pure HTML until needed.

Backend Response Times

API response times (p95):

  • /api/posts: 45ms
  • /api/work-projects: 38ms
  • /api/profile: 22ms

Why fast:

  • Hono is lightweight
  • PostgreSQL is local (same Docker network)
  • No ORM overhead (direct Directus SDK)

The Developer Experience

Hot Reload: Actually Hot

Change a component → See it in <100ms

Why:

  • Astro's fast refresh
  • Docker volume mounts (no rebuild needed)
  • Minimal build pipeline

The difference: Tight feedback loop. Iterate 10x in an hour, not a day.

Type Safety Across Boundaries

// Directus schema
interface WorkProject { title: string }

// API layer mirrors it
interface WorkProject { title: string }

// Frontend mirrors it
interface WorkProject { title: string }

Misspell title anywhere → Build fails.

What this prevented: 5-6 bugs that would have been "why is this undefined?" in production.

Error Messages That Actually Help

Docker error:

network homelab-network declared as external, but could not be found

Fix: docker network create homelab-network

Directus error:

Field "slug" is required but not provided

Fix: Add slug to the request.

Why this matters: Debugging is fast when errors are clear.

What This Enables

Content Updates Without Deployment

Before: Edit markdown → Commit → Push → Build → Deploy → 5 minutes

Now: Edit in Directus → Save → Instant

Real example: Fixed a typo in my bio. Took 10 seconds. No deployment.

Easy to Extend

Want to add a "talks" section?

  1. Create talks collection in Directus (5 minutes)
  2. Add API endpoint in Hono (2 lines)
  3. Create Astro page (copy/paste work page pattern)
  4. Done

Time: 15 minutes. No refactoring. No breaking changes.

Self-Hosting Without Pain

This runs in my closet on a Debian server.

Costs:

  • Hardware: $200 one-time (mini PC)
  • Electricity: ~$3/month
  • Cloudflare Tunnel: Free

vs Vercel/Netlify:

  • Free tier limits: 100GB bandwidth, then $20/100GB
  • Pro tier: $20/month minimum

Break-even: After 3 months, self-hosting is cheaper.

The Honest Trade-offs

What I Lost

React Interactivity: Can't build complex SPAs. Fine for a portfolio.

Vercel Niceties: No preview deploys. No automatic HTTPS per branch. But I didn't need them.

Managed Database: I maintain PostgreSQL myself. But Docker makes it trivial.

What I Gained

Speed: 97 Lighthouse score. <1s interactive.

Control: My data, my server, my rules.

Cost: $3/month electricity vs $20/month hosting.

Flexibility: Can run anything on this server, not just web apps.

The Results

Live: https://b28.dev

Performance:

  • Lighthouse: 97
  • FCP: <0.8s
  • TTI: <1.2s

DX:

  • Hot reload: <100ms
  • Type-safe: Build fails on errors
  • One-command setup: docker compose up

Maintainability:

  • Content updates: 10 seconds
  • Add new sections: 15 minutes
  • Deploy changes: git pull && docker compose up -d

Cost:

  • Development: 10 hours active work
  • Hosting: $3/month electricity
  • Maintenance: ~1 hour/month

The Takeaway

These weren't random choices. Each tool delivered specific value:

  • Astro: Speed (12KB JS, 97 Lighthouse)
  • Directus: Non-developer updates
  • Hono: Type-safe API, edge-ready
  • Docker: Identical dev/prod
  • Cloudflare Tunnels: Zero-config HTTPS

The pattern: Choose tools for what they deliver, not what's popular.


Template credit: Felix Macaspac's v0.dev minimalist portfolio

The velocity shift: Right tools → Fast site, fast updates, fast iteration