Last updated on

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

  1. The Architecture: Why This Stack?
  2. Setting Up Astro
  3. Integrating Decap CMS
  4. Netlify Identity & Git Gateway
  5. The Production Workflow
  6. Performance Optimization
  7. 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:

  1. It commits to Git
  2. Triggers a Netlify build
  3. Deploys the new static site
  4. 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:

MetricScore
Performance100
Accessibility100
Best Practices100
SEO100

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:

  1. Development Speed: From idea to deployed site in 2 days
  2. Zero Maintenance: No server updates, no database migrations
  3. Perfect Performance: 100/100 Lighthouse out of the box
  4. Git-Based Workflow: Everything is version-controlled

Gotchas to Watch Out For:

  1. Git Gateway Must Be Enabled: Without it, /admin won’t authenticate
  2. CORS Issues: Make sure your Netlify site URL matches in config.yml
  3. Build Time: Large blogs (1000+ posts) will take longer to build
  4. 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:

  1. Clone a starter: npm create astro@latest — —template blog
  2. Add Decap CMS: Follow the integration steps above
  3. Deploy to Netlify: Push to GitHub and connect
  4. Enable Git Gateway: This is the critical step most tutorials skip