A modern, SEO-optimized portfolio website showcasing professional experience, projects, speaking engagements, and education.
🌐 Live Site: navarrolajous.com | www.navarrolajous.com
- Overview
- ✨ Features
- 🛠️ Tech Stack
- 📁 Project Structure
- 🗄️ Data Architecture
- 🚀 Getting Started
- 💻 Development Workflow
- ➕ Adding Content
- 🎨 Customization
- 📜 Available Scripts
- 🌐 Deployment
- 🤝 Contributing
- 📄 License
- 📧 Contact
This is a professional portfolio website built with Next.js 16 (App Router) and TypeScript, featuring a modern design system with dark mode support, comprehensive SEO, and dynamic content sections. The site showcases professional experience, project portfolio, academic background, and speaking engagements at conferences.
Key Highlights:
- 📱 Fully responsive design (mobile-first approach)
- 🌓 Dark/Light theme with system detection
- 🔍 SEO optimized with sitemap, robots.txt, and Schema.org structured data
- ⚡ Static generation for optimal performance
- 📊 Integrated Umami Analytics
- 📧 Contact form with Resend email integration
- 🎤 Speaking engagements and conference talks showcase
- 🤖 LLM-friendly endpoints (
/llms.txt,/llms-full.txt) - ♿ Accessibility features (skip-to-content navigation)
- Jobs & Startups: Separate tabs for employment history and entrepreneurial ventures
- Detail Pages: Individual pages for each role with dynamic routes (
/experience/[slug]) - Rich Content: Responsibilities, achievements, tech stack, location, and period
- Company Links: Direct links to company websites and profiles
- Three Categories: Freelance, Hobby, and Open Source projects
- Detail Pages: Comprehensive project showcase (
/projects/[slug]) - Project Details: Features, challenges, impact, screenshots, and banners
- Links: GitHub repositories and live project URLs
- Visual Content: Project banners and multiple screenshots
- Conference Presentations: Showcase of talks at DuneCon, DeFi Security Summit, etc.
- Detail Pages: Full talk information with descriptions and topics (
/talks/[slug]) - Media Integration: YouTube video embeds and PDF slide viewers
- Event Information: Date, location, and event details
- Academic Background: Degrees and certifications
- Detail Pages: In-depth educational credentials (
/education/[slug]) - Academic Details: Thesis information, coursework, achievements, and tech stack
- Contact Form: Validated form with Zod schema
- Email Integration: Automated email notifications via Resend API
- User Feedback: Toast notifications for form submission status
- 📄 Resume Download: Direct PDF download from header navigation
- 🔗 Social Links: GitHub, LinkedIn, and Twitter integration
- 🎨 Theme Toggle: Smooth dark/light mode switching with persistence
- 📱 Mobile Navigation: Responsive header with mobile menu
- 🔍 SEO: Meta tags, OpenGraph, Twitter cards, and JSON-LD structured data
- 🗺️ Sitemap: Auto-generated sitemap for all routes
- 🤖 Robots.txt: Search engine crawler configuration (blocks API routes from crawlers)
- 📈 Analytics: Umami Analytics integration for privacy-friendly tracking
- 🤖 LLM Endpoints:
/llms.txtand/llms-full.txtfor AI-friendly site context - 📰 Newsletter CTA: Optional Substack newsletter subscription on homepage
- ♿ Accessibility: Skip-to-content link for keyboard navigation
- ✨ Animations: Staggered fade-in-up entrance animations on homepage
- Next.js 16 - React framework with App Router
- React 19 - UI library
- TypeScript 5.9 - Type-safe JavaScript (strict mode)
- Tailwind CSS 3.4 - Utility-first CSS framework
- tailwindcss-animate - Animation utilities
- tailwind-merge - Utility for merging Tailwind classes
- shadcn/ui - Re-usable component library
- Radix UI - Unstyled accessible components
- Dialog, Dropdown Menu, Label, Navigation Menu, Slot, Tabs, Toast, Avatar
- Lucide React - Icon library
- class-variance-authority - CVA for component variants
- next-themes - Dark mode implementation
- react-hook-form - Form handling
- Zod 4 - Schema validation
- @hookform/resolvers - Form validation resolvers
- Resend - Email API for contact form
- React Email - Email templates as React components
- clsx - Conditional classNames utility
- ESLint 9 - Code linting with JSDoc enforcement (
eslint-plugin-jsdoc) - PostCSS - CSS processing
- Autoprefixer - CSS vendor prefixes
- Vercel - Hosting and deployment platform
- Umami Analytics - Privacy-friendly analytics
website/
├── app/ # Next.js App Router
│ ├── api/
│ │ └── contact/
│ │ └── route.ts # Contact form API endpoint
│ ├── contact/
│ │ ├── components/
│ │ │ └── ContactForm.tsx # Contact form component
│ │ └── page.tsx # Contact page
│ ├── education/
│ │ ├── [slug]/
│ │ │ └── page.tsx # Education detail pages
│ │ ├── components/
│ │ │ └── EducationCard.tsx
│ │ └── page.tsx # Education list page
│ ├── experience/
│ │ ├── [slug]/
│ │ │ └── page.tsx # Experience detail pages
│ │ ├── components/
│ │ │ ├── ExperienceCard/
│ │ │ └── ExperiencesTab/
│ │ ├── layout.tsx # Experience layout for metadata
│ │ └── page.tsx # Experience list with tabs
│ ├── projects/
│ │ ├── [slug]/
│ │ │ └── page.tsx # Project detail pages
│ │ ├── components/
│ │ │ ├── ProjectCard/
│ │ │ └── ProjectTabs/
│ │ ├── layout.tsx # Projects layout for metadata
│ │ └── page.tsx # Projects list with tabs
│ ├── talks/
│ │ ├── [slug]/
│ │ │ └── page.tsx # Talk detail pages
│ │ ├── components/
│ │ │ ├── PDFViewer/
│ │ │ ├── TalkCard/
│ │ │ └── YouTubeEmbed/
│ │ └── page.tsx # Talks list page
│ ├── llms.txt/
│ │ └── route.ts # /llms.txt LLM-friendly endpoint
│ ├── llms-full.txt/
│ │ └── route.ts # /llms-full.txt extended LLM endpoint
│ ├── icon.png # App icon
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── robots.ts # Robots.txt generation
│ └── sitemap.ts # Sitemap generation
│
├── components/ # Shared components
│ ├── emails-templates/ # Email templates for Resend
│ ├── Footer/
│ ├── Header/ # Navigation header
│ │ ├── MobileNavigation/
│ │ └── NavigationLink/
│ ├── icons/ # Icon components
│ │ ├── Logo/
│ │ ├── Github/
│ │ └── MailIcon.tsx
│ ├── social/ # Social link components
│ │ ├── SocialLink.tsx # Base social link component
│ │ ├── GitHubLink.tsx
│ │ ├── LinkedInLink.tsx
│ │ ├── TwitterLink.tsx
│ │ ├── EmailLink.tsx
│ │ └── WebsiteLink.tsx
│ ├── ModeToggle/ # Dark/light theme toggle
│ ├── JsonLd.tsx # Server-rendered Schema.org JSON-LD renderer
│ ├── ui/ # shadcn/ui components
│ └── theme-provider.tsx # Theme provider wrapper
│
├── constants/ # Application constants
│ └── routes.ts # Route definitions and SITE_URL config
│
├── domains/ # TypeScript interfaces/types
│ ├── Education.ts # Education interface
│ ├── Experience.ts # Experience interface
│ ├── Project.ts # Project interface
│ └── Talk.ts # Talk interface
│
├── lib/ # Library utilities
│ ├── utils.ts # Tailwind utility (cn function + blurDataURL)
│ └── schema.ts # Schema.org JSON-LD builders
│
├── public/ # Static assets
│ ├── assets/ # Images, banners, etc.
│ └── resume.pdf # Downloadable resume
│
├── services/ # Data services (static data)
│ ├── contacts/ # Contact service
│ ├── education.ts # Education data
│ ├── experience.ts # Jobs, startups, skills data
│ ├── projects.ts # Projects data (freelance, hobby, opensource)
│ └── talks.ts # Talks and presentations data
│
├── utils/ # Utility functions
│ ├── getEnv.ts # Environment variable helper
│ ├── iconUtils.ts # Icon utilities
│ └── llms.ts # LLM text generation for /llms.txt routes
│
├── .claude/ # Claude Code configuration
│ ├── agents/ # Reusable agent definitions
│ │ ├── release-validator.md # Validates release readiness
│ │ ├── qa-executor.md # Executes QA test plans
│ │ └── pr-reviewer.md # Expert code review
│ ├── commands/ # Custom slash commands
│ │ ├── start.md # /start - Create feature branch
│ │ ├── commit.md # /commit - Stage and commit
│ │ ├── sync.md # /sync - Sync with main
│ │ ├── build.md # /build - Build and validate
│ │ ├── release.md # /release - Create release
│ │ ├── dev.md # /dev - Start dev server
│ │ ├── finish.md # /finish - Create PR
│ │ ├── release-notes.md # /release-notes - Enhance release
│ │ ├── review.md # /review - Code review
│ │ ├── setup.md # /setup - MCP server setup
│ │ ├── rfc.md # /rfc - Create RFC document
│ │ ├── tdd.md # /tdd - Test-driven development
│ │ ├── plan-qa.md # /plan-qa - Generate QA test plan
│ │ ├── start-qa.md # /start-qa - Execute QA tests
│ │ └── update.md # /update - Update commands from source
│ └── CLAUDE.md # AI assistant guidance
│
├── .env.template # Environment variables template
├── .nvmrc # Node version (v22.12.0)
├── components.json # shadcn/ui configuration
├── eslint.config.mjs # ESLint configuration with JSDoc enforcement
├── next.config.mjs # Next.js configuration
├── package.json # Dependencies and scripts
├── postcss.config.js # PostCSS configuration
├── tailwind.config.ts # Tailwind configuration
└── tsconfig.json # TypeScript configuration
This project uses a static TypeScript data pattern where all content is stored as typed data in the services/ directory. This approach provides:
- ✅ Type safety for all content
- ✅ Easy content management
- ✅ Fast builds (static generation)
- ✅ No database needed
All content types are defined as TypeScript interfaces in domains/:
// domains/Experience.ts
export interface Experience {
id: number;
slug: string;
type: 'job' | 'startup';
company: string;
position: string;
period: string;
location?: string;
companyUrl?: string;
responsibilities: string[];
achievements?: string[];
technologies: string[];
banner?: string;
}
// domains/Project.ts
export interface Project {
id: number;
slug: string;
type: 'freelance' | 'hobby' | 'opensource';
name: string;
company: string;
github?: string;
website?: string;
period: string;
description: string;
detailedDescription?: string;
features?: string[];
challenges?: string[];
technologies: string[];
banner?: string;
screenshots?: string[];
impact?: string;
}Content is stored in services/ files as typed arrays:
// services/experience.ts
import { Experience } from "@/domains/Experience";
export const jobs: Experience[] = [
{
id: 1,
slug: "webacy",
type: "job",
company: "Webacy",
position: "Staff Software Engineer",
period: "Jan 2023 — Present",
// ... more fields
},
// ... more jobs
];
export const startups: Experience[] = [
// ... startups
];- Node.js 22+: Version specified in
.nvmrc(usenvm useif you have nvm installed) - npm: Comes with Node.js
- Resend Account: For contact form email functionality (sign up at resend.com)
-
Clone the repository:
git clone https://github.com/rlajous/website.git cd website -
Set up Node.js version:
nvm use
If you don't have nvm, ensure you're using the Node.js version from
.nvmrc. -
Install dependencies:
npm install
-
Configure environment variables:
cp .env.template .env.local
Update
.env.localwith your values:# Resend API Key (get from https://resend.com/api-keys) RESEND_API_KEY=re_your_api_key_here # Email recipient for contact form RECIPIENT_EMAIL=your.email@example.com # Optional: Override site URL for different environments NEXT_PUBLIC_SITE_URL=https://navarrolajous.com # Optional: Substack publication URL for newsletter CTA on homepage NEXT_PUBLIC_SUBSTACK_PUBLICATION_URL=https://yourname.substack.com
-
Start the development server:
npm run dev
-
Open your browser: Navigate to http://localhost:3000
Issue: Module not found errors
- Solution: Delete
node_modulesand.next, then runnpm installagain
Issue: Environment variables not working
- Solution: Restart the dev server after changing
.env.local - Ensure variables start with
NEXT_PUBLIC_for client-side access
Issue: Build fails
- Solution: Run
npm run buildlocally to see detailed error messages - Check TypeScript errors with
npm run lint
This project uses custom slash commands (in .claude/commands/) for streamlined development:
-
/start- Start a new PR by creating a feature branch- Creates branch following naming convention (
feat/,fix/, etc.) - Stores context for other commands
- Creates branch following naming convention (
-
/commit- Stage and commit changes with proper formatting- Follows conventional commit format
- Adds co-author attribution
-
/sync- Sync your feature branch with latest main- Prevents merge conflicts
- Keeps branch up to date
-
/build- Build and validate the production bundle locally- Runs
npm run build - Validates output
- Runs
-
/dev- Start the development server with environment validation- Checks environment variables
- Starts
npm run dev
-
/finish- Create a pull request with comprehensive description- Generates PR description
- Creates PR on GitHub
-
/release- Create a new release version and trigger deployment- Bumps version (patch/minor/major)
- Creates git tag
- Triggers Vercel deployment
-
/release-notes- Enhance GitHub release with detailed notes- Generates categorized changelog
- Updates GitHub release
-
/review- Comprehensive code review- Analyzes across 8 dimensions (correctness, security, performance, etc.)
- Reports issues by severity
-
/setup- Interactive MCP server setup- Configures Linear, Jira, or GitHub Issues integration
-
/rfc- Create RFC documents- Auto-numbering and template instantiation
-
/tdd- Test-driven development workflow- RED-GREEN-REFACTOR cycle with test execution
-
/plan-qa- Generate QA test plan- Creates YAML test plan from ticket descriptions or requirements
-
/start-qa- Execute QA tests- Runs tests from a generated QA plan file and reports results
-
/update- Update commands and agents from source repo- Supports
--dry-run,--prune,--forceoptions
- Supports
The project includes reusable agent definitions (in .claude/agents/):
release-validator- Validates release readiness (tests, build, dependencies, changelog)qa-executor- Executes QA test plans with detailed reportingpr-reviewer- Expert code review for quality, security, and best practices
Branch Naming Convention:
feat/feature-name # New features
fix/bug-description # Bug fixes
refactor/what-changed # Code improvements
content/what-updated # Content updates
design/what-improved # Design changes
docs/what-documented # Documentation
Commit Message Format:
[ Type ] Description
Examples:
[ Feature ] Add blog section with MDX support
[ Bug ] Fix mobile navigation overflow
[ Content ] Update experience with Webacy role
[ Design ] Improve dark mode theme transitions
[ Refactor ] Simplify contact form validation
- Open
services/experience.ts - Add a new entry to the
jobsorstartupsarray:
export const jobs: Experience[] = [
{
id: 8, // Increment ID
slug: "company-name", // URL-friendly slug
type: "job",
company: "Company Name",
position: "Your Position",
period: "Jan 2024 — Present",
location: "Remote", // Optional
companyUrl: "https://company.com", // Optional
responsibilities: [
"Key responsibility 1",
"Key responsibility 2",
"Key responsibility 3",
],
achievements: [ // Optional
"Achievement 1",
"Achievement 2",
],
technologies: ["React", "TypeScript", "Node.js"],
banner: "/experience/company-banner.jpg", // Optional
},
// ... existing jobs
];- Add banner image to
public/assets/experience/(if using) - The detail page will be auto-generated at
/experience/company-name
- Open
services/projects.ts - Add to
freelance,hobby, oropensourcearray:
export const freelance: Project[] = [
{
id: 8, // Increment ID
slug: "project-name",
type: "freelance",
name: "Project Name",
company: "Client Name",
github: "https://github.com/user/repo", // Optional
website: "https://project.com", // Optional
period: "2024",
description: "Brief description for card view",
detailedDescription: "Longer description for detail page", // Optional
features: [ // Optional
"Feature 1",
"Feature 2",
],
challenges: [ // Optional
"Challenge 1",
"Challenge 2",
],
technologies: ["Next.js", "Tailwind CSS"],
banner: "/projects/project-banner.jpg", // Optional
screenshots: [ // Optional
"/projects/project-screenshot-1.jpg",
"/projects/project-screenshot-2.jpg",
],
impact: "Impact description", // Optional
},
// ... existing projects
];- Add images to
public/assets/projects/ - Detail page will be at
/projects/project-name
- Open
services/talks.ts - Add a new talk:
export const talks: Talk[] = [
{
id: 3, // Increment ID
slug: "event-name-year",
title: "Talk Title",
event: "Conference Name",
location: "City, Country",
date: "2025-03-15",
description: "Talk description and key points",
topics: ["Topic 1", "Topic 2", "Topic 3"],
slides: "https://slides.com/presentation", // Optional
video: "https://youtube.com/watch?v=...", // Optional
banner: "/talks/event-banner.jpg", // Optional
},
// ... existing talks
];- Add banner to
public/assets/talks/ - Detail page will be at
/talks/event-name-year
- Open
services/education.ts - Add new entry:
export const education: Education[] = [
{
id: 3, // Increment ID
slug: "degree-institution",
degree: "Degree Name",
institution: "University Name",
period: "2020 — 2024",
location: "City, Country", // Optional
institutionUrl: "https://university.edu", // Optional
specialization: "Specialization", // Optional
thesis: { // Optional
title: "Thesis Title",
description: "Thesis description",
link: "https://thesis-link.com",
},
coursework: ["Course 1", "Course 2"], // Optional
achievements: ["Achievement 1"], // Optional
technologies: ["Python", "R", "MATLAB"],
banner: "/education/institution-banner.jpg", // Optional
},
// ... existing education
];Edit app/globals.css to modify the color palette:
@layer base {
:root {
--primary: 222.2 47.4% 11.2%; /* Change primary color */
--secondary: 210 40% 96.1%; /* Change secondary color */
/* ... other colors */
}
.dark {
--primary: 210 40% 98%; /* Dark mode primary */
/* ... other colors */
}
}Components follow the shadcn/ui pattern. To modify a component:
- Find it in
components/ui/ - Edit the component file
- Maintain the CVA pattern for variants
- Create a new directory in
app/ - Add
page.tsx:export default function NewPage() { return <div>New Page Content</div> }
- Add to navigation in
components/Header/ - Update
constants/routes.ts
Edit app/layout.tsx for global metadata:
export const metadata: Metadata = {
title: "Your Name | Title",
description: "Your description",
// ... other metadata
};For page-specific metadata, add generateMetadata to page files.
# Development
npm run dev # Start development server (http://localhost:3000)
# Production
npm run build # Build for production
npm start # Start production server
# Code Quality
npm run lint # Run ESLintThis site is optimized for Vercel deployment with automatic CI/CD:
-
Fork/Clone this repository
-
Connect to Vercel:
- Go to vercel.com
- Click "New Project"
- Import your repository
- Vercel will auto-detect Next.js
-
Configure Environment Variables: In Vercel project settings, add:
RESEND_API_KEY=your_resend_api_key RECIPIENT_EMAIL=your.email@example.com NEXT_PUBLIC_SITE_URL=https://yourdomain.com NEXT_PUBLIC_SUBSTACK_PUBLICATION_URL=https://yourname.substack.com # Optional -
Configure Domains:
- Add your custom domain in Vercel project settings
- Update DNS records as instructed by Vercel
- Both apex and www domains are supported
-
Deploy:
- Push to
mainbranch to trigger deployment - Vercel builds and deploys automatically
- Preview deployments created for pull requests
- Push to
- Analytics: Configure Umami Analytics in
app/layout.tsx - Sitemap: Automatically generated at
/sitemap.xml - Robots: Configured at
/robots.txt - Monitoring: Check Vercel dashboard for build logs and analytics
Contributions are welcome! Whether you're fixing bugs, adding features, or improving documentation, your help is appreciated.
-
Fork the repository
-
Create a feature branch:
git checkout -b feat/amazing-feature
-
Make your changes:
- Follow the existing code style
- Add types for all TypeScript code
- Test your changes locally
- Update documentation if needed
-
Commit your changes:
git commit -m "[ Feature ] Add amazing feature" -
Push to your fork:
git push origin feat/amazing-feature
-
Open a Pull Request
- TypeScript: Use strict mode, add types for all variables
- Components: Follow shadcn/ui patterns
- Naming: Use descriptive names, camelCase for variables/functions
- Comments: Add comments for complex logic
- Formatting: Use Prettier (if configured) or follow existing style
This project is licensed under the MIT License - see the LICENSE file for details.
Rodrigo Manuel Navarro Lajous
- 🌐 Website: navarrolajous.com
- 💼 LinkedIn: linkedin.com/in/rodrigo-lajous
- 🐙 GitHub: @rlajous
- 🐦 Twitter: @ro_lajous