Building My Personal Portfolio: A Modern Static Site with Next.js and AWS
Building My Personal Portfolio: A Modern Static Site with Next.js and AWS
After 25+ years of building enterprise applications for others, I decided it was time to create something for myself—a personal portfolio website that showcases my experience, projects, and expertise. This blog post documents the journey of building cre47e.com from concept to deployment.
Why Build a Personal Portfolio?
As a SaaS Application Architect, I've spent my career designing systems for Fortune 500 companies and startups. However, I realized I didn't have a centralized place to:
- Showcase my 25+ years of experience
- Share my thoughts on software architecture
- Demonstrate my technical skills
- Connect with the developer community
A personal portfolio seemed like the perfect solution—a project that would allow me to practice what I preach about modern web development while creating something useful.
Technology Stack Selection
Core Framework: Next.js 14
I chose Next.js 14 for several reasons:
- Static Site Generation (SSG): Perfect for a portfolio site that doesn't need dynamic server-side rendering
- Excellent Developer Experience: Hot reloading, TypeScript support, and great documentation
- Performance: Automatic code splitting and optimization
- File-based Routing: Simple and intuitive for a portfolio structure
- React Ecosystem: Leverages my existing React knowledge
Styling: Tailwind CSS
Tailwind CSS was the natural choice for styling:
- Utility-first approach: Rapid development without writing custom CSS
- Responsive design: Built-in breakpoints make mobile-first design easy
- Dark theme: Perfect for a modern, professional look
- Customization: Easy to extend with custom colors and animations
Deployment: AWS S3 + CloudFront
Given my extensive AWS experience, I chose:
- S3: Cost-effective static hosting (~$1-3/month)
- CloudFront CDN: Global distribution for fast loading times
- Route53: Domain management (already had this set up)
- GitHub Actions: Automated CI/CD pipeline
Architecture Decisions
Static Export Configuration
I configured Next.js for static export:
// next.config.js
const nextConfig = {
output: 'export',
trailingSlash: true,
images: {
unoptimized: true,
},
};
Why trailingSlash: true?
- Ensures all routes end with
/, which works better with S3 static hosting - Prevents issues with CloudFront routing
Component Architecture
I organized components by feature:
src/
├── components/
│ ├── Header.tsx # Navigation
│ ├── Hero.tsx # Landing section
│ ├── About.tsx # About section
│ ├── Experience.tsx # Career timeline
│ ├── Projects.tsx # Project showcase
│ ├── Skills.tsx # Technical skills
│ ├── Contact.tsx # Contact form
│ ├── Footer.tsx # Footer
│ └── ThemeToggle.tsx # Theme switcher
Each component is self-contained and reusable, following React best practices.
Blog System: Markdown-Based CMS
For the blog, I implemented a simple file-based CMS:
// src/lib/blog.ts
export function getAllPosts(): BlogPost[] {
const fileNames = fs.readdirSync(postsDirectory);
// Parse markdown files with gray-matter
// Return sorted posts
}
Benefits:
- No database needed
- Version controlled content
- Easy to write and edit
- Fast static generation
Key Features Implemented
1. Dynamic Theme System
One of the most interesting features I built was a theme toggle that cycles through 8 different color schemes:
const themes = [
{ name: 'Midnight Blue', primary: '59, 130, 246', ... },
{ name: 'Forest Night', primary: '16, 185, 129', ... },
// ... 6 more themes
];
Each theme changes:
- Primary accent colors
- Background gradients
- Card backgrounds
- All with smooth CSS transitions
This demonstrates modern CSS techniques using CSS variables and transitions.
2. Interactive Experience Timeline
The experience section uses an interactive timeline where users can click through different positions. This makes it easy to explore my 25+ year career without overwhelming the page.
3. Project Filtering
Projects are organized by category (Fintech, Data Engineering, IoT, Enterprise) with a filter system that updates the display dynamically.
4. Contact Form (Lambda + SES)
The contact form submits to an AWS Lambda function that sends email via Amazon SES:
- Lambda Function URL: Public HTTPS endpoint with CORS enabled (no API Gateway needed)
- CORS: Configured only in the Function URL settings in AWS—the Lambda code does not send CORS headers, to avoid duplicate values (see challenges below)
- Environment:
RECIPIENT_EMAIL,SENDER_EMAIL; the frontend gets the Lambda URL fromNEXT_PUBLIC_LAMBDA_CONTACT_URLat build time via GitHub Secrets
5. Responsive Design
The entire site is mobile-first and responsive:
- Collapsible navigation on mobile
- Stacked layouts for smaller screens
- Touch-friendly buttons and interactions
Deployment Challenges and Solutions
Challenge 1: CloudFront 404 on Initial Load
Problem: When visiting the root URL, CloudFront was showing a 404 page instead of index.html.
Root Cause: With Origin Access Control (OAC), S3 returns 403 (not 404) when a file doesn't exist. CloudFront wasn't configured to handle this.
Solution: Configured CloudFront Error Pages:
- 403 →
/index.htmlwith HTTP 200 (critical!) - 404 →
/404.htmlwith HTTP 404
This ensures the root URL always serves the homepage correctly.
Challenge 2: Trailing Slash Routing
Problem: Next.js with trailingSlash: true creates routes like /blog/ but CloudFront needed to serve /blog/index.html.
Solution:
- Verified
trailingSlash: trueinnext.config.js - Ensured CloudFront default root object is
index.html - Added CloudFront Function to handle directory paths (optional but recommended)
Challenge 3: GitHub Actions Deployment
Problem: Setting up automated deployment from GitHub to S3.
Solution: Created a GitHub Actions workflow:
- name: Sync to S3
run: |
aws s3 sync out/ s3://${{ secrets.S3_BUCKET_NAME }} \
--delete \
--cache-control "public, max-age=31536000, immutable"
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
Key Learnings:
- Use different cache-control for HTML vs static assets
- Always invalidate CloudFront cache after deployment
- Store AWS credentials as GitHub Secrets (Repository secrets, not Environment)
Challenge 4: Contact Form CORS
Problem: The contact form showed "Failed to fetch" or "Network error" on production. Later, emails were delivered but the UI still showed an error. The browser console reported: "The 'Access-Control-Allow-Origin' header contains multiple values 'https://cre47e.com, https://cre47e.com', but only one is allowed."
Root Cause: CORS was configured in two places: (1) Lambda Function URL in the AWS Console, and (2) the Lambda handler code sending Access-Control-Allow-Origin in the response. The browser received the header twice and blocked the response—even though the request succeeded and the email was sent.
Solution: Configure CORS in one place only: Lambda → Configuration → Function URL → Edit → CORS (Allow origins, methods, headers). Removed all CORS headers from the Lambda response; the handler now returns only Content-Type: application/json. The Function URL layer adds the correct CORS headers automatically.
Takeaway: With Lambda Function URLs, let AWS handle CORS in the Function URL settings. Do not send Access-Control-Allow-Origin (or other CORS headers) from your Lambda code, or you'll get duplicate values and the browser will block the response.
Performance Optimizations
1. Static Generation
All pages are pre-rendered at build time, resulting in:
- First Load JS: ~98 KB (excellent!)
- No server-side processing: Instant page loads
- SEO-friendly: All content is in HTML
2. Code Splitting
Next.js automatically splits code:
- Main bundle: 87.4 KB
- Page-specific chunks: ~10 KB each
- Shared chunks loaded once
3. Image Optimization
While I used unoptimized: true for S3 compatibility, in production you could:
- Use Next.js Image component with a custom loader
- Or pre-optimize images during build
4. CSS Optimization
Tailwind CSS purges unused styles, resulting in a minimal CSS bundle.
Cost Analysis
Monthly AWS Costs:
| Service | Usage | Cost | |---------|-------|------| | S3 Storage | ~5 MB | $0.01 | | S3 Requests | ~10K | $0.50 | | CloudFront | ~50 GB | $4.25 | | Route53 | 1 hosted zone | $0.50 | | Total | | ~$5-6/month |
Extremely cost-effective for a professional portfolio!
Lessons Learned
1. Static Sites Are Powerful
You don't always need a backend. For a portfolio, static generation provides:
- Better performance
- Lower costs
- Easier maintenance
- Better security (no server to hack)
2. AWS Configuration Matters
CloudFront error page configuration is critical for SPA/static sites. The 403 → 200 redirect is a must-have.
3. CORS: One Source of Truth
For Lambda Function URLs, set CORS only in the Function URL configuration in AWS. If your Lambda code also sends CORS headers, the response will have duplicate Access-Control-Allow-Origin values and the browser will block it—even when the request and email send succeed.
4. Developer Experience Counts
Next.js + Tailwind CSS made development enjoyable:
- Fast iteration
- Great tooling
- Excellent documentation
- Strong community
5. CI/CD Simplifies Deployment
GitHub Actions made deployment trivial:
- Push to main → automatic deployment
- No manual S3 uploads
- Automatic cache invalidation
Future Enhancements
Some ideas for future improvements:
- Analytics: Add privacy-friendly analytics (Plausible or self-hosted)
- Search: Implement client-side search for blog posts
- Comments: Add comment system (maybe using GitHub Discussions API)
- Newsletter: Integrate email newsletter signup
- Dark/Light Mode: Add persistent theme preference (localStorage)
- Performance Monitoring: Add Web Vitals tracking
Conclusion
Building this portfolio was a rewarding experience. It allowed me to:
- Practice modern web development techniques
- Showcase my skills and experience
- Learn about AWS static site hosting
- Create something I'm proud to share
The site is now live at cre47e.com, and I'm excited to continue adding content and features.
If you're considering building your own portfolio, I highly recommend the Next.js + AWS stack. It's powerful, cost-effective, and provides an excellent developer experience.
Tech Stack Summary:
- Framework: Next.js 14 (Static Export)
- Styling: Tailwind CSS
- Language: TypeScript
- Hosting: AWS S3 + CloudFront
- Contact Form: AWS Lambda (Function URL) + Amazon SES
- CI/CD: GitHub Actions
- Domain: Route53
- Blog: Markdown files
Total Development Time: ~2 days
Monthly Cost: ~$5-6
Performance: 98 KB First Load JS, <1s load time
Have questions about building your own portfolio? Feel free to reach out on LinkedIn or email me!