A modern, responsive personal portfolio and blog website built with Astro, featuring internationalization (English/Spanish/Portuguese), optimized performance, and comprehensive SEO.
Live Site: jjuanrivvera.com
- Internationalization (i18n) - Full English, Spanish, and Portuguese support with URL-based routing (
/for EN,/es/for ES,/pt/for PT) - Dark Theme Design - Modern dark UI with vibrant accent colors and gradient effects
- Responsive Layout - Mobile-first design with hamburger menu for mobile devices
- SPA-like Navigation - Astro View Transitions for smooth page transitions without full reloads
- Smooth Animations - AOS (Animate On Scroll) library for scroll-triggered animations
- Accessibility - Skip link, keyboard navigation (Escape to close menu), focus styles, reduced motion support
- Performance Optimized - Image optimization, font preloading, LCP preload, CSS/JS bundling
- SEO Ready - Open Graph, Twitter Cards, JSON-LD structured data, dynamic hreflang, sitemap
- Blog System - MDX-powered blog with pagination, tags, related posts, reading time, table of contents, and RSS feeds
- Astro - Static site generator with component islands
- TypeScript - Type-safe translations and utilities
- CSS3 - Custom properties, flexbox, grid, animations
- AOS - Animate On Scroll library
- Vitest - Unit testing framework for utilities and components
- Playwright - E2E testing framework
- Inter Font - Self-hosted web font
- Netlify - Hosting with automatic deployments
website/
├── src/
│ ├── assets/
│ │ └── img/ # Optimized images (processed by Astro)
│ ├── components/
│ │ ├── layout/
│ │ │ ├── Navbar.astro # Navigation with language switcher
│ │ │ └── Footer.astro # Footer component
│ │ ├── sections/
│ │ │ ├── Hero.astro # Hero section
│ │ │ ├── Experience.astro # Work experience timeline
│ │ │ ├── Skills.astro # Technical skills grid
│ │ │ ├── Projects.astro # Featured projects
│ │ │ ├── Education.astro # Academic background
│ │ │ └── Contact.astro # Contact information
│ │ └── ErrorPage.astro # 404 error page component
│ ├── i18n/
│ │ ├── ui.ts # Translation strings (EN/ES)
│ │ └── utils.ts # i18n utilities (translations, URLs, hreflang)
│ ├── layouts/
│ │ └── Layout.astro # Main layout with meta tags, scripts
│ ├── pages/
│ │ ├── index.astro # English homepage
│ │ ├── 404.astro # English 404 page
│ │ ├── es/
│ │ │ ├── index.astro # Spanish homepage
│ │ │ └── 404.astro # Spanish 404 page
│ │ │ ├── blog/
│ │ │ │ ├── [...page].astro # Spanish blog listing
│ │ │ │ ├── [slug].astro # Spanish blog posts
│ │ │ │ └── tag/
│ │ │ │ └── [tag].astro # Spanish tag archives
│ │ │ └── rss.xml.ts # Spanish RSS feed
│ │ └── pt/
│ │ ├── index.astro # Portuguese homepage
│ │ └── 404.astro # Portuguese 404 page
│ │ ├── blog/
│ │ │ ├── [...page].astro # Portuguese blog listing
│ │ │ ├── [slug].astro # Portuguese blog posts
│ │ │ └── tag/
│ │ │ └── [tag].astro # Portuguese tag archives
│ │ └── rss.xml.ts # Portuguese RSS feed
│ └── styles/
│ └── global.css # Global styles
├── content/
│ └── blog/
│ ├── en/ # English blog posts (MDX)
│ ├── es/ # Spanish blog posts (MDX)
│ └── pt/ # Portuguese blog posts (MDX)
├── config/
│ ├── blog.ts # Blog configuration
│ └── site.ts # Site configuration
├── types/
│ └── blog.ts # Blog type definitions
├── utils/
│ ├── blogHreflang.ts # Cross-language blog linking
│ ├── blogSchema.ts # JSON-LD schema generation
│ ├── dateFormatting.ts # Localized date formatting
│ ├── postSorting.ts # Blog post sorting utilities
│ ├── readingTime.ts # Reading time calculation
│ ├── relatedPosts.ts # Related posts algorithm
│ ├── slug.ts # URL slug generation
│ └── toc.ts # Table of contents parsing
├── content.config.ts # Content collections configuration
├── tests/
│ ├── e2e/
│ │ ├── blog.spec.ts # Blog E2E tests
│ │ ├── global-setup.ts # Test configuration
│ │ └── i18n.spec.ts # i18n E2E tests
│ └── unit/
│ ├── components/
│ │ └── blog/
│ │ ├── ReadingTime.test.ts
│ │ └── TagList.test.ts
│ ├── i18n/
│ │ └── utils.test.ts
│ └── utils/
│ ├── blogHreflang.test.ts
│ ├── dateFormatting.test.ts
│ ├── readingTime.test.ts
│ └── relatedPosts.test.ts
├── public/
│ ├── css/
│ │ └── fonts.css # Font-face declarations
│ ├── fonts/
│ │ └── Inter*.woff2 # Self-hosted Inter font files
│ ├── img/
│ │ ├── icons.svg # SVG sprite for icons
│ │ └── favicon.ico # Favicon
│ ├── files/
│ │ └── CV.pdf # Downloadable CV
│ └── robots.txt # Search engine directives
├── astro.config.mjs # Astro configuration
├── tsconfig.json # TypeScript configuration
├── netlify.toml # Netlify deployment config
└── package.json # Dependencies and scripts
| Section | Description |
|---|---|
| Hero | Introduction with profile image, title, CTAs, and social links |
| Experience | Professional work history timeline with tech tags |
| Skills | Technical skills organized by category (Frontend, Backend, Database, Cloud) |
| Projects | Featured projects with descriptions and technologies |
| Education | Academic background |
| Contact | Contact information with email and location |
| Blog | Blog post listings with pagination, tag filtering, and search |
| Post | Individual blog posts with reading time, table of contents, and social sharing |
- Node.js 20+
- pnpm 9+
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Build for production
pnpm build
# Preview production build
pnpm preview
# Type checking
pnpm astro checkThe site uses the following environment variables:
URL: Base URL for the site (used for canonical URLs and sitemaps). Set to your production domain (e.g.,https://jjuanrivvera.com)
In development, Astro uses localhost:4321 by default. For production builds, set the URL environment variable.
| Command | Description |
|---|---|
pnpm dev |
Start dev server at localhost:4321 |
pnpm build |
Type-check and build for production |
pnpm preview |
Preview production build locally |
pnpm check |
Run TypeScript diagnostics |
pnpm format |
Format code with Prettier |
pnpm format:check |
Check code formatting |
pnpm lint |
Lint code with ESLint |
pnpm test |
Run all tests (unit + E2E sequentially) |
pnpm test:unit |
Run Vitest unit tests |
pnpm test:unit:watch |
Run unit tests in watch mode |
pnpm test:unit:ui |
Open Vitest UI |
pnpm test:e2e |
Run Playwright E2E tests |
pnpm test:e2e:ui |
Open Playwright UI |
pnpm test:coverage |
Generate test coverage report |
The project uses a comprehensive testing strategy with both unit and E2E tests:
tests/
├── unit/ # Vitest unit tests (~1s execution)
│ ├── components/
│ │ └── blog/ # Component tests using Astro Container API
│ ├── i18n/ # Internationalization utilities
│ └── utils/ # Pure utility functions
└── e2e/ # Playwright E2E tests (~10s execution)
├── blog.spec.ts # Blog functionality tests
├── i18n.spec.ts # Language switching tests
└── global-setup.ts # Test configuration
115 tests covering:
- Utility Functions - Reading time calculation, date formatting, related posts algorithm
- i18n Utilities - Language detection, URL generation, translations
- Blog Utilities - Hreflang generation, tag filtering, post sorting
- Components - ReadingTime and TagList components using Astro Container API
# Run unit tests
pnpm test:unit
# Watch mode for development
pnpm test:unit:watch
# Open Vitest UI
pnpm test:unit:ui59 tests covering:
- Homepage rendering and navigation across all languages
- Language switcher functionality
- Mobile menu interactions
- Section visibility and content
- 404 page handling
- Blog listing, post rendering, and navigation
- SEO meta tags and hreflang tags
# Run E2E tests
pnpm test:e2e
# Open Playwright UI
pnpm test:e2e:ui- Unit tests run first in CI for fast feedback on logic errors
- E2E tests run in parallel with unit tests for faster CI execution
- Coverage reporting available via
pnpm test:coverage - All tests must pass before deployment
The blog uses Astro's content collections API with MDX for rich content authoring.
Blog posts are written in MDX and stored in src/content/blog/{lang}/ with the following frontmatter:
---
title: 'Post Title'
description: 'Post description for SEO'
pubDate: 2024-01-01
author: 'Juan Felipe Rivera'
tags: ['tag1', 'tag2']
cover:
src: '/img/blog/covers/post-cover.jpg'
alt: 'Cover image alt text'
language: 'en'
translationKey: 'post-slug'
featured: false
---- Pagination: 12 posts per page with navigation
- Tags: Filter posts by tags with dedicated archive pages
- Related Posts: Algorithm-based recommendations using tags and recency
- Reading Time: Automatic calculation based on word count
- Table of Contents: Generated from headings with anchor links
- Social Sharing: Twitter, LinkedIn, and copy link buttons
- RSS Feeds: Auto-generated per language at
/rss.xml
- Create a new
.mdfile in the appropriate language folder - Use standard Markdown with optional JSX components
- Add frontmatter with required fields
- For translations, use the same
translationKeyacross languages - Run
pnpm buildto validate content
The site supports English (default), Spanish, and Portuguese with URL-based routing:
| Language | URL Pattern | Example |
|---|---|---|
| English | / |
jjuanrivvera.com/ |
| Spanish | /es/ |
jjuanrivvera.com/es/ |
| Portuguese | /pt/ |
jjuanrivvera.com/pt/ |
Translations are defined in src/i18n/ui.ts (UI elements) and src/i18n/blog.ts (blog-specific strings) with TypeScript for type safety:
// Access translations in components
const t = useTranslations(lang);
const title = t('hero.name');- Add the key to the
en,es, andptobjects insrc/i18n/ui.ts - Use
t('your.key')in components
- Dynamic Canonical URLs - Each page has its own canonical URL
- hreflang Tags - Proper alternate language links for each page
- Open Graph - Social sharing metadata with optimized images
- Twitter Cards - Large image cards for Twitter
- JSON-LD - Person schema for structured data
- Sitemap - Auto-generated with i18n support (
sitemap-index.xml) - robots.txt - Search engine directives
- Image Optimization - Astro's built-in image processing with WebP output
- LCP Preload - Profile picture preloaded for faster rendering
- Font Preload - Critical fonts preloaded
- CSS Bundling - Styles bundled and minified
- JS Code Splitting - Only load what's needed per page
- View Transitions - SPA-like navigation without full reloads
- Skip link for keyboard navigation
- Escape key closes mobile menu
- Focus-visible styles for keyboard users
- Reduced motion support via
prefers-reduced-motion - ARIA labels on interactive elements
- Semantic HTML structure
Google Analytics 4 is integrated with privacy-focused configuration:
- IP anonymization enabled
- Tracked events:
- Page views
- CV downloads
- Email clicks
- Social media clicks (LinkedIn, GitHub)
- Section visibility (IntersectionObserver)
The site is configured for Netlify deployment:
- Connect your repository to Netlify
- Build settings are auto-detected from
netlify.toml:- Build command:
pnpm run build - Publish directory:
dist - Node version: 20
- pnpm version: 9
- Build command:
# Build the site
pnpm build
# The `dist/` folder contains the static site
# Upload to any static hosting provider- Content Security Policy (CSP) meta tag
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- Referrer-Policy: strict-origin-when-cross-origin
- Blog Posts: Follow the content management guidelines above
- Translations: Add new keys to all language objects in
src/i18n/ui.tsandsrc/i18n/blog.ts - Components: Use existing patterns and run
pnpm lintandpnpm format
- Create a feature branch from
main - Make changes and test locally
- Run
pnpm buildandpnpm testto ensure everything works - Commit with conventional style (e.g.,
feat: add new blog feature) - Open a PR with description and screenshots if UI changes
- Add tests for new features
- Update this README if adding new features
- Ensure accessibility and i18n coverage
All rights reserved.
Built with Astro