Building a Lightning-Fast Portfolio with Astro and Tailwind CSS

Introduction
In today’s digital landscape, having a fast, accessible, and visually appealing portfolio website is crucial for developers and designers. This tutorial will guide you through building a modern portfolio using Astro for static site generation and Tailwind CSS for styling.
Why Astro + Tailwind CSS?
Astro’s Benefits
- Zero JavaScript by default - Pages load instantly
- Component Islands - Interactive components only when needed
- Multiple framework support - Use React, Vue, Svelte components
- Built-in optimizations - Image optimization, code splitting, and more
Tailwind CSS Benefits
- Rapid development - Utility-first approach speeds up styling
- Consistent design - Pre-built design system
- Small bundle size - Only includes used utilities
- Responsive by default - Mobile-first responsive design
Project Setup
1. Initialize Astro Project
npm create astro@latest my-portfolio -- --template minimal
cd my-portfolio
2. Install Dependencies
npm install @astrojs/tailwind @astrojs/mdx
npm install -D tailwindcss @tailwindcss/typography
3. Configure Tailwind CSS
// tailwind.config.mjs
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
darkMode: 'class',
theme: {
extend: {
colors: {
accent: {
DEFAULT: '#3B82F6',
dark: '#2563EB',
light: '#60A5FA'
}
}
},
},
plugins: [require('@tailwindcss/typography')],
}
4. Update Astro Configuration
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import mdx from '@astrojs/mdx';
export default defineConfig({
integrations: [tailwind(), mdx()],
site: 'https://yourdomain.com',
output: 'static'
});
Project Structure
src/
├── components/ # Reusable components
├── content/ # Markdown content
│ ├── blog/ # Blog posts
│ └── projects/ # Project pages
├── layouts/ # Page layouts
├── pages/ # Route pages
└── styles/ # Global styles
Building the Layout
1. Create Base Layout
---
// src/layouts/Layout.astro
export interface Props {
title: string;
description?: string;
}
const { title, description = "My portfolio website" } = Astro.props;
---
<!DOCTYPE html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<title>{title}</title>
</head>
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<slot />
</body>
</html>
2. Create Navigation Component
---
// src/components/Navigation.astro
const navItems = [
{ href: '/', label: 'Home' },
{ href: '/projects', label: 'Projects' },
{ href: '/blog', label: 'Blog' },
{ href: '/contact', label: 'Contact' }
];
---
<nav class="bg-white dark:bg-gray-800 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<a href="/" class="text-xl font-bold text-accent">
Your Name
</a>
</div>
<div class="hidden md:flex items-center space-x-8">
{navItems.map(item => (
<a
href={item.href}
class="text-gray-700 dark:text-gray-300 hover:text-accent transition-colors"
>
{item.label}
</a>
))}
</div>
</div>
</div>
</nav>
Content Management
1. Set Up Content Collections
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
author: z.string(),
tags: z.array(z.string()).optional(),
}),
});
const projects = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
category: z.string(),
tags: z.array(z.string()),
featured: z.boolean().optional(),
}),
});
export const collections = { blog, projects };
2. Create Dynamic Routes
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
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();
---
<Layout title={post.data.title}>
<article class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-4">{post.data.title}</h1>
<div class="prose dark:prose-invert max-w-none">
<Content />
</div>
</article>
</Layout>
Performance Optimizations
1. Image Optimization
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<Image
src={heroImage}
alt="Hero image"
class="w-full h-64 object-cover rounded-lg"
format="webp"
quality={80}
/>
2. Lazy Loading
<img
src="/image.jpg"
alt="Description"
loading="lazy"
class="w-full h-64 object-cover"
/>
3. Font Optimization
---
// In your layout
---
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
Dark Mode Implementation
1. Add Dark Mode Toggle
---
// src/components/ThemeToggle.astro
---
<button
id="theme-toggle"
class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700"
aria-label="Toggle dark mode"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<!-- Sun icon -->
</svg>
</button>
<script>
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
window.localStorage.setItem('theme', theme);
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
}
document.getElementById("theme-toggle").addEventListener("click", handleToggleClick);
</script>
SEO Optimization
1. Add Meta Tags
---
// In your layout
export interface Props {
title: string;
description: string;
image?: string;
}
const { title, description, image } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
{image && <meta property="og:image" content={image} />}
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
</head>
2. Generate Sitemap
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://yourdomain.com',
integrations: [sitemap()],
});
Deployment
1. Cloudflare Pages
# Build the project
npm run build
# Deploy to Cloudflare Pages
npx wrangler pages deploy dist
2. GitHub Actions (Optional)
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run build
- uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: your-project-name
directory: dist
Performance Testing
1. Lighthouse Scores
After deployment, test your site with Lighthouse:
npx lighthouse https://yourdomain.com --output html
2. Core Web Vitals
Monitor your Core Web Vitals in Google Search Console and PageSpeed Insights.
Best Practices
1. Component Design
- Keep components small and focused
- Use TypeScript for type safety
- Implement proper accessibility attributes
- Test components in isolation
2. Performance
- Optimize images and use modern formats
- Minimize JavaScript bundle size
- Implement proper caching strategies
- Use lazy loading for non-critical resources
3. Accessibility
- Use semantic HTML elements
- Provide proper alt text for images
- Ensure keyboard navigation works
- Test with screen readers
Conclusion
Building a portfolio with Astro and Tailwind CSS provides an excellent foundation for a fast, maintainable, and scalable website. The combination of static generation, utility-first CSS, and modern tooling creates a developer experience that’s both productive and performant.
Key takeaways:
- Performance first - Static generation provides excellent performance out of the box
- Developer experience - Hot reloading and TypeScript support speed up development
- Maintainability - Component-based architecture makes the codebase easy to maintain
- Scalability - The architecture supports growth and new features
Resources
Start building your portfolio today and showcase your work with a lightning-fast, beautiful website!