How to Build a Portfolio with Astro | Developer Portfolio Guide

Build a fast, beautiful developer portfolio with Astro featuring project showcases, a blog section, and perfect Lighthouse scores.

~3-4 hoursbeginner7 steps
Share:XLinkedIn

Your portfolio is your most important marketing tool as a developer or designer. Astro is the perfect framework for portfolios because it ships zero JavaScript by default, giving you a 100/100 Lighthouse score without any optimization work. You can still use React, Vue, or Svelte components where you need interactivity. This guide builds a complete portfolio with project showcases, an about page, a contact form, and smooth view transitions.

Prerequisites

  • Node.js 20+

    Required to run Astro and its build tools.

  • Basic HTML and CSS

    Astro components use HTML-like syntax with scoped styles, so HTML and CSS knowledge is essential.

  • Project Screenshots or Mockups

    Prepare images or screenshots of 3-5 projects you want to showcase in your portfolio.

01

Create the Astro Project with Tailwind CSS

Scaffold a minimal Astro project and add Tailwind CSS for styling. Unlike the blog template, start with the minimal template so you control the entire structure. Add the sitemap integration for SEO and the image integration for automatic optimization.

bash
npm create astro@latest my-portfolio -- --template minimal
cd my-portfolio
npx astro add tailwind
npx astro add sitemap
npm install

Tip: The minimal template gives you a clean slate without any preset pages or components.

Tip: Set your site URL in astro.config.mjs immediately — it is needed for sitemap generation and canonical URLs.

02

Create the Base Layout with Navigation

Build a reusable layout component with a responsive navigation bar, SEO meta tags, and a footer. Astro layouts use slots to wrap page content, similar to React children. The layout includes Open Graph tags for social sharing and a mobile-friendly hamburger menu.

src/layouts/BaseLayout.astroastro
---
// src/layouts/BaseLayout.astro
interface Props {
  title: string;
  description: string;
  image?: string;
}

const { title, description, image = "/og-image.jpg" } = Astro.props;
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{title}</title>
    <meta name="description" content={description} />
    <link rel="canonical" href={canonicalUrl} />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={new URL(image, Astro.site)} />
    <meta property="og:type" content="website" />
  </head>
  <body class="bg-white text-gray-900 antialiased">
    <header class="fixed top-0 z-50 w-full border-b bg-white/80 backdrop-blur-sm">
      <nav class="mx-auto flex max-w-5xl items-center justify-between px-4 py-4">
        <a href="/" class="text-xl font-bold">YourName</a>
        <ul class="flex gap-8 text-sm font-medium">
          <li><a href="/#projects" class="hover:text-blue-600">Projects</a></li>
          <li><a href="/about" class="hover:text-blue-600">About</a></li>
          <li><a href="/contact" class="hover:text-blue-600">Contact</a></li>
        </ul>
      </nav>
    </header>
    <main class="pt-16">
      <slot />
    </main>
    <footer class="border-t py-8 text-center text-sm text-gray-500">
      <p>&copy; {new Date().getFullYear()} YourName. All rights reserved.</p>
    </footer>
  </body>
</html>

Tip: Use backdrop-blur-sm on the navbar for a frosted glass effect that keeps content readable underneath.

Tip: Add View Transitions later with a single component import for smooth page navigation.

03

Define a Project Content Collection

Create a content collection for your portfolio projects with a typed schema. Each project has a title, description, tech stack, links, images, and display order. Content collections give you type safety and build-time validation for all your project data.

src/content.config.tstypescript
// src/content.config.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const projects = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/content/projects" }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    tech: z.array(z.string()),
    image: z.string(),
    liveUrl: z.string().url().optional(),
    repoUrl: z.string().url().optional(),
    featured: z.boolean().default(false),
    order: z.number().default(0),
  }),
});

export const collections = { projects };

Tip: Use the 'featured' flag to highlight your best 2-3 projects on the homepage.

Tip: The 'order' field lets you control project display order without renaming files.

04

Build the Homepage with Hero and Project Grid

Create the homepage with a hero section introducing yourself and a grid of featured projects. The hero section should communicate who you are, what you do, and include a call-to-action. The project grid uses your content collection to display project cards with hover effects.

src/pages/index.astroastro
---
// src/pages/index.astro
import BaseLayout from "../layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
import { Image } from "astro:assets";

const projects = (await getCollection("projects"))
  .sort((a, b) => a.data.order - b.data.order);
const featured = projects.filter((p) => p.data.featured);
---

<BaseLayout title="YourName — Developer" description="Full-stack developer building modern web applications.">
  <section class="mx-auto max-w-5xl px-4 py-24">
    <p class="text-sm font-medium uppercase tracking-wider text-blue-600">Full-Stack Developer</p>
    <h1 class="mt-4 text-5xl font-bold leading-tight md:text-6xl">
      I build web applications<br />that people love to use.
    </h1>
    <p class="mt-6 max-w-xl text-lg text-gray-600">
      Specializing in React, Next.js, and TypeScript. Currently available for
      freelance projects and full-time opportunities.
    </p>
    <div class="mt-8 flex gap-4">
      <a href="/contact" class="rounded-lg bg-black px-6 py-3 text-white hover:bg-gray-800">
        Get in touch
      </a>
      <a href="/#projects" class="rounded-lg border px-6 py-3 hover:bg-gray-50">
        View projects
      </a>
    </div>
  </section>

  <section id="projects" class="mx-auto max-w-5xl px-4 py-16">
    <h2 class="mb-12 text-3xl font-bold">Featured Projects</h2>
    <div class="grid gap-8 md:grid-cols-2">
      {featured.map((project) => (
        <a
          href={`/projects/${project.id}`}
          class="group overflow-hidden rounded-xl border transition hover:shadow-xl"
        >
          <img
            src={project.data.image}
            alt={project.data.title}
            class="aspect-video w-full object-cover transition group-hover:scale-105"
          />
          <div class="p-6">
            <h3 class="text-xl font-semibold">{project.data.title}</h3>
            <p class="mt-2 text-gray-600">{project.data.description}</p>
            <div class="mt-4 flex flex-wrap gap-2">
              {project.data.tech.map((t) => (
                <span class="rounded bg-gray-100 px-2 py-1 text-xs">{t}</span>
              ))}
            </div>
          </div>
        </a>
      ))}
    </div>
  </section>
</BaseLayout>
05

Create Individual Project Pages

Build detail pages for each project using Astro's dynamic routing. Each page shows the full project description, tech stack, screenshots, and links to the live site and source code. Use getStaticPaths to generate a page for every project in your collection at build time.

src/pages/projects/[id].astroastro
---
// src/pages/projects/[id].astro
import { getCollection, render } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";

export async function getStaticPaths() {
  const projects = await getCollection("projects");
  return projects.map((project) => ({
    params: { id: project.id },
    props: { project },
  }));
}

const { project } = Astro.props;
const { Content } = await render(project);
---

<BaseLayout title={project.data.title} description={project.data.description}>
  <article class="mx-auto max-w-3xl px-4 py-16">
    <a href="/#projects" class="text-sm text-gray-500 hover:text-gray-700">
      &larr; Back to projects
    </a>
    <h1 class="mt-6 text-4xl font-bold">{project.data.title}</h1>
    <div class="mt-4 flex flex-wrap gap-2">
      {project.data.tech.map((t) => (
        <span class="rounded bg-gray-100 px-3 py-1 text-sm">{t}</span>
      ))}
    </div>
    <img
      src={project.data.image}
      alt={project.data.title}
      class="mt-8 w-full rounded-xl"
    />
    <div class="mt-6 flex gap-4">
      {project.data.liveUrl && (
        <a href={project.data.liveUrl} target="_blank" rel="noopener"
          class="rounded-lg bg-black px-6 py-2 text-white hover:bg-gray-800">
          View Live
        </a>
      )}
      {project.data.repoUrl && (
        <a href={project.data.repoUrl} target="_blank" rel="noopener"
          class="rounded-lg border px-6 py-2 hover:bg-gray-50">
          Source Code
        </a>
      )}
    </div>
    <div class="prose prose-lg mt-8 max-w-none">
      <Content />
    </div>
  </article>
</BaseLayout>

Tip: Write detailed case studies in the Markdown body — explain the problem, your approach, and the results.

Tip: Add before/after screenshots or performance comparisons to demonstrate impact.

06

Add View Transitions for Smooth Navigation

Enable Astro's built-in View Transitions API for smooth, app-like page navigation without a JavaScript framework. Pages morph between each other with crossfade animations, and you can add per-element transitions for project images that expand from the grid to the detail page.

src/layouts/BaseLayout.astroastro
---
// Add to src/layouts/BaseLayout.astro <head>
import { ViewTransitions } from "astro:transitions";
---

<!-- Inside <head> -->
<ViewTransitions />

<!-- On project card images, add transition:name -->
<img
  src={project.data.image}
  alt={project.data.title}
  class="aspect-video w-full object-cover"
  transition:name={`project-${project.id}`}
/>

<!-- On project detail page image, use same transition:name -->
<img
  src={project.data.image}
  alt={project.data.title}
  class="mt-8 w-full rounded-xl"
  transition:name={`project-${project.id}`}
/>

Tip: Use the same transition:name on elements across pages to create morphing animations between them.

Tip: View Transitions work in Chrome, Edge, and Safari — Firefox falls back to standard navigation gracefully.

07

Deploy to Vercel or Netlify

Deploy your portfolio to a global CDN for near-instant page loads worldwide. Astro's static output works on any hosting platform. Connect your custom domain and set up automatic deployments from your Git repository so every push updates your live site.

bash
# Build and verify locally
npm run build
npm run preview

# Deploy to Vercel
npx astro add vercel
npx vercel --prod

# Or deploy to Netlify
npx astro add netlify
npx netlify deploy --prod --dir=dist

Tip: Set up a custom domain — yourname.dev or yourname.com — for a professional impression.

Tip: Add Plausible or Fathom analytics to track which projects get the most views without compromising visitor privacy.

Tip: Run a Lighthouse audit after deploying — Astro portfolios should score 95+ across all categories.

Next Steps

  • Add a blog section to your portfolio to demonstrate expertise and improve SEO.
  • Implement a dark mode toggle with Tailwind's dark: variant and localStorage persistence.
  • Add a resume/CV page with a downloadable PDF version.
  • Integrate a headless CMS like Tina for editing project content without touching code.