Building a Modern Portfolio with Astro, Decap CMS, and Netlify: A Complete JAMstack Guide
As a Full Stack Developer, I’ve built complex applications using Ruby on Rails and React, managing databases, servers, and infrastructure. However, for my personal portfolio, I wanted something fundamentally different: a stack that was blazing fast by default, required zero maintenance, and was completely serverless.
I chose the “JAMstack” trinity: Astro for the frontend, Decap CMS (formerly Netlify CMS) for content management, and Netlify for hosting and CI/CD.
This post documents exactly how I built this site, serving as a production-ready guide for anyone looking to build a high-performance blog that scores 100/100 on Lighthouse.
Table of Contents
- The Architecture: Why This Stack?
- Setting Up Astro
- Integrating Decap CMS
- Netlify Identity & Git Gateway
- The Production Workflow
- Performance Optimization
- Lessons Learned
1. The Architecture: Why This Stack?
Before writing a single line of code, I evaluated my requirements:
Performance: Must score 100/100 on Lighthouse
Maintenance: No databases, no security patches
Cost: Ideally free to host
DX (Developer Experience): Write in Markdown, manage via UI
Why Astro?
Astro is a web framework that ships zero JavaScript to the client by default. Unlike React (which hydrates the entire page), Astro only hydrates interactive “islands.” This means:
- Static pages load instantly
- No unnecessary JavaScript bundle
- Perfect Core Web Vitals scores
Example code:
---
// Only this component will be interactive
import Counter from '../components/Counter.jsx';
---
<html>
<body>
<h1>This is static HTML</h1>
<Counter client:load /> <!-- Only this hydrates -->
</body>
</html>
Why Decap CMS?
Decap CMS is a Git-based CMS. Instead of a database, it saves blog posts as .md files directly into your GitHub repository. Benefits:
- No database to manage or secure
- All content is version-controlled
- Can edit content via Git or a beautiful UI
- Works entirely client-side (SPA)
Why Netlify?
Netlify provides the build pipeline. Every time I save a post in Decap CMS:
- It commits to Git
- Triggers a Netlify build
- Deploys the new static site
- All in 45 seconds
2. Setting Up Astro
Installation
I started by initializing an Astro project:
npm create astro@latest codewithmk
cd codewithmk
npm install
Content Collections
Astro uses a src/content directory for managing collections. I defined a schema to ensure every blog post has strict TypeScript typing:
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).optional(),
}),
});
export const collections = { blog };
This gives me compile-time safety. If I forget to add a title, TypeScript will throw an error.
Dynamic Routing
Astro’s file-based routing makes it trivial to create dynamic blog post pages:
---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.slug },
props: post,
}));
}
const post = Astro.props;
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
Key insight: getStaticPaths() runs at build time, not runtime. This means every blog post is pre-rendered as static HTML.
3. Integrating Decap CMS
This is where the magic happens. Decap CMS is a Single Page Application (SPA) that lives in your /admin folder. It doesn’t run on a server; it runs in the browser and talks to the GitHub API.
Step 1: Create Admin Files
I created two files in public/admin/:
public/admin/index.html (The entry point):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Content Manager</title>
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
</head>
<body>
<script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
</body>
</html>
public/admin/config.yml (The CMS configuration):
backend:
name: git-gateway
branch: main
publish_mode: editorial_workflow
media_folder: "public/images"
public_folder: "/images"
collections:
- name: "blog"
label: "Blog Posts"
folder: "src/content/blog"
create: true
slug: "{{slug}}"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Description", name: "description", widget: "text" }
- { label: "Publish Date", name: "pubDate", widget: "datetime" }
- { label: "Hero Image", name: "heroImage", widget: "image", required: false }
- { label: "Tags", name: "tags", widget: "list", required: false }
- { label: "Body", name: "body", widget: "markdown" }
This YAML file tells Decap:
- Where to save files: src/content/blog
- What fields to show: Title, description, date, etc.
- How to authenticate: via git-gateway
4. Netlify Identity & Git Gateway
The most common “gotcha” with this stack is authentication. Since there’s no backend server, how do we log in to /admin?
Solution: Netlify Identity + Git Gateway
Step-by-Step Setup:
1. Deploy to Netlify
- Connect your GitHub repo
- Netlify auto-detects Astro and builds it
2. Enable Identity
- Go to Site Settings > Identity
- Click “Enable Identity”
3. Enable Git Gateway (Critical!)
- Scroll down to Services > Git Gateway
- Click “Enable Git Gateway”
This allows Netlify to act as a proxy between your authenticated user and GitHub, granting permission to commit changes without exposing your GitHub token.
4. Invite Yourself
- Go to Identity > Invite users
- Add your email
- You’ll receive a confirmation email
Add the Widget
Add this script to your index.astro (or your layout):
<script>
if (window.netlifyIdentity) {
window.netlifyIdentity.on("init", user => {
if (!user) {
window.netlifyIdentity.on("login", () => {
document.location.href = "/admin/";
});
}
});
}
</script>
5. The Production Workflow
Now, the loop is closed. Here’s what happens when I write a blog post:
What Actually Happens:
Step 1: I go to codewithmk.com/admin and log in
Step 2: Write my post in the rich text editor with live preview
Step 3: Click “Publish”
Step 4: Decap CMS uses the Git Gateway API to push a new commit like “Create new post: building-with-jamstack.md”
Step 5: Netlify detects the commit and fires a build hook
Step 6: Astro builds the static HTML (including the new post)
Step 7: New version is live in about 45 seconds
Editorial Workflow (Optional)
Decap supports a draft → review → publish workflow:
publish_mode: editorial_workflow
This creates three columns:
- Drafts: Work in progress
- In Review: Ready for approval
- Ready: Will be published on next merge
6. Performance Optimization
Results:
| Metric | Score |
|---|---|
| Performance | 100 |
| Accessibility | 100 |
| Best Practices | 100 |
| SEO | 100 |
How I Achieved This:
1. Image Optimization
Astro’s Image component automatically:
- Converts to WebP
- Generates multiple sizes
- Lazy loads by default
---
import { Image } from 'astro:assets';
import heroImg from '../assets/hero.png';
---
<Image src={heroImg} alt="Hero" />
2. Zero JavaScript by Default
Unless I explicitly add client:load, components are static HTML:
<Header /> <!-- Static HTML -->
<Counter client:load /> <!-- Hydrated JS -->
3. Prefetching
Astro prefetches links on hover:
<a href="/blog" data-astro-prefetch>Blog</a>
4. Asset Optimization
// astro.config.mjs
export default defineConfig({
build: {
inlineStylesheets: 'auto',
},
});
7. Lessons Learned
What Went Well:
- Development Speed: From idea to deployed site in 2 days
- Zero Maintenance: No server updates, no database migrations
- Perfect Performance: 100/100 Lighthouse out of the box
- Git-Based Workflow: Everything is version-controlled
Gotchas to Watch Out For:
- Git Gateway Must Be Enabled: Without it, /admin won’t authenticate
- CORS Issues: Make sure your Netlify site URL matches in config.yml
- Build Time: Large blogs (1000+ posts) will take longer to build
- Media Management: Decap stores images in your repo (use Git LFS for large sites)
Production Tips:
Set Build Environment Variables:
# Netlify build settings
NODE_VERSION=18
Add a Build Hook:
Go to Site Settings > Build & Deploy > Build Hooks and create a hook. You can trigger rebuilds via API:
curl -X POST -d '{}' https://api.netlify.com/build_hooks/YOUR_HOOK_ID
Conclusion
This stack represents the modern way to build content-driven sites. It combines:
- The developer experience of Git-flow
- The ease of use of a CMS
- The performance of static sites
For developers preparing for technical interviews, building your own platform like this demonstrates:
- Understanding of headless architectures
- Knowledge of CI/CD pipelines
- Mastery of static site generation (SSG)
- Ability to choose and justify architectural decisions
Next Steps:
If you want to build something similar, here’s what I recommend:
- Clone a starter: npm create astro@latest — —template blog
- Add Decap CMS: Follow the integration steps above
- Deploy to Netlify: Push to GitHub and connect
- Enable Git Gateway: This is the critical step most tutorials skip