Personal Portfolio Website
A static portfolio and blog built with Astro and MDX, deployed on Cloudflare Pages with CI/CD.
Overview
This is my personal portfolio website — the very site you’re looking at right now. It serves as a hub for my professional profile, project showcases, and technical blog posts. Built with Astro and authored in MDX, it prioritises speed, simplicity, and ease of content updates.
Motivation
I wanted a personal site that:
- Loads instantly with zero client-side JavaScript by default
- Lets me write content in Markdown/MDX — no JSX wrangling for blog posts
- Costs $0/month to host
- Deploys automatically on every
git push
Tech Stack
| Layer | Choice |
|---|---|
| Framework | Astro (static output) |
| Content | MDX via Astro Content Collections |
| Language | TypeScript (strict mode) |
| Styling | Vanilla CSS with custom properties |
| Deployment | Cloudflare Pages |
| CI/CD | Cloudflare Pages (auto-build) |
Architecture Decisions
Why Astro over Next.js or Gatsby?
For a content-heavy site with no interactive UI, shipping a JavaScript runtime is unnecessary. Astro’s static output produces pure HTML + CSS pages that score 100 on Lighthouse performance out of the box.
Content Collections for type safety
All blog posts and project write-ups are typed with Zod schemas through Astro Content Collections. Frontmatter errors are caught at build time, not after deployment.
const blogs = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
Centralised profile data
Education history, work experience, certifications, and social links are defined in a single profile.ts file. Every page that displays profile information reads from this source, eliminating duplication and keeping updates to one place.
Deployment Pipeline
The site is connected to Cloudflare Pages, which watches the main branch. On every push:
- Cloudflare pulls the latest code
- Runs
pnpm buildto generate static files indist/ - Deploys to Cloudflare’s global CDN
- SSL and custom domain are handled automatically
Total hosting cost: $0/month.
Lessons Learned
- Start simple. I initially explored Backblaze B2 + Cloudflare CDN as a hosting setup. The S3-compatible endpoint required Host header overrides (an Enterprise-only Cloudflare feature), and the native
/file/endpoint needed a Cloudflare Worker for path rewriting. Cloudflare Pages eliminated all of this complexity in minutes. - Static-first pays off. Zero-JS pages mean no hydration bugs, no loading states, and near-perfect Lighthouse scores without any optimisation work.
- Content Collections are worth the setup. Catching a missing frontmatter field at build time is far better than discovering a broken page in production.
Source code is available on GitHub.