Personal portfolio and blog built with Astro v5 and Tailwind CSS v4.
View Live Site • Features • Tech Stack • Getting Started • Project Structure
- Multilingual -- English (default) and Spanish with full i18n support, per-locale RSS feeds, and hreflang tags
- Blog -- MDX-powered blog with pagination, reading time, social sharing, and Giscus comments
- SEO -- Canonical URLs, Open Graph, Twitter Cards, JSON-LD structured data (Person, WebSite, ProfilePage, BlogPosting, BreadcrumbList), auto-generated OG images, sitemap with hreflang
- Dark mode -- Three-state theme toggle (auto/light/dark) with blocking script to prevent flash of wrong theme
- Accessibility -- Skip links, ARIA labels, focus-visible indicators, prefers-reduced-motion support, screen reader announcements
- Performance -- Zero JS by default, inlined CSS, subsetted variable font, Brotli + gzip compression, lazy-loaded images with AVIF/WebP and responsive srcset
- View Transitions -- Native CSS view transitions with lazy transition-name assignment for smooth page navigation
| Technology | Description |
|---|---|
| Astro v5 | Static site generator with zero JS by default |
| Tailwind CSS v4 | Utility-first CSS framework |
| TypeScript | Type-safe JavaScript |
| MDX | Markdown with components for blog posts |
| Tool | Description |
|---|---|
| Bun | Fast JavaScript runtime and package manager |
| OxLint | High-performance linter with type-aware rules |
| Prettier | Code formatter with Astro and Tailwind plugins |
| Husky | Git hooks |
| Commitlint | Conventional commit linting |
| lint-staged | Run linters on staged files |
| Step | Description |
|---|---|
| Astro | Static page generation with inlined CSS |
| astro-og-canvas | Build-time OG image generation per blog post |
| @playform/compress | HTML, CSS, JS, JSON, SVG, and image minification |
| astro-compressor | Brotli and gzip pre-compression |
| SVGO | SVG optimization via Astro experimental flag |
| Workflow | Description |
|---|---|
| Verify | Lint, format check, and build on PRs |
| Release | Semantic-release with Cloudflare Pages deployment |
| CodeQL | Security vulnerability scanning |
| Lighthouse | Performance and accessibility audits (>= 90%) |
| Commitlint | Validates conventional commit format |
| Dependabot | Weekly automated dependency updates |
- Bun >= 1.3
git clone https://github.com/LuisUrrutia/website.git
cd website
bun installbun run devThe site will be available at http://localhost:4321 with hot module replacement.
| Command | Description |
|---|---|
bun run dev |
Start development server at localhost:4321 with HMR |
bun run build |
Build optimized production site to dist/ |
bun run preview |
Preview the production build locally |
bun run build:preview |
Build and preview in one command |
bun run lint |
Run OxLint with type-aware rules |
bun run fmt |
Check code formatting with Prettier |
bun run fmt:fix |
Auto-fix formatting issues |
A docker-compose.yml is provided to test the production build with Static Web Server, which supports Brotli compression and proper cache headers:
bun run build
docker compose up -d
# Site available at http://localhost:8080src/
├── assets/
│ ├── blog/ # Blog post images
│ ├── icons/ # SVG icons (optimized via SVGO)
│ ├── images/ # Site images (processed by Astro)
│ └── testimonials/ # Testimonial photos
├── components/ # Reusable UI components (.astro)
│ ├── mdx/ # Custom MDX components
│ └── seo/ # JSON-LD, meta tags, Open Graph
├── content/
│ └── blog/ # MDX blog posts (en/ and es/)
├── data/ # Static data (blog, seo, technologies, companies)
├── i18n/ # Translations and i18n utilities
├── integrations/ # Custom Astro integrations (sitemap hreflang)
├── layouts/ # Layout.astro
├── lib/ # Utilities (formatters, theme, social-share)
├── pages/ # File-based routes
│ ├── blog/ # Blog listing, pagination, and post pages
│ ├── es/ # Spanish locale routes
│ ├── og/ # Auto-generated OG images
│ ├── robots.txt.ts # Dynamic robots.txt
│ └── rss.xml.ts # RSS feed
├── sections/ # Page sections (Hero, Stack, Testimonials, Contact)
├── styles/ # Theme tokens (oklch), global CSS, utilities
├── types/ # TypeScript definitions
└── views/ # Page compositions (HomePage, BlogPage, BlogPostPage)
public/
├── companies/ # Company logo SVGs (light + dark variants)
├── favicons/ # Favicon files (SVG, PNG at multiple sizes)
├── fonts/ # Self-hosted Inter Variable (subsetted woff2)
├── images/ # Static images
└── tech/ # Technology icon SVGs
Pages are thin routing files that delegate to Views, which compose Sections and Components. This avoids duplication between English and Spanish routes:
Page (/pages/index.astro) → View (HomePage.astro) → Sections (HeroSection, StackSection, ...)
Page (/pages/es/index.astro) → View (HomePage.astro) → same sections, different locale
Colors use oklch for perceptual uniformity and P3 gamut support. Semantic tokens reference primitives and are defined in src/styles/theme.css:
- Light theme on
:root, dark theme on[data-theme="dark"] - Three-state toggle: auto (follows system), light, dark
- Persisted in
localStorage, applied via blocking<head>script
- Locales: English (no URL prefix) and Spanish (
/es/) - Translations:
src/i18n/ui.tswith type-safe keys - Access:
Astro.locals.t("key")andAstro.locals.lang - Blog translations linked via
translationSlugfrontmatter field
CI enforces minimum Lighthouse scores (configurable in lighthouserc.json):
- Performance: 90%
- Accessibility: 90%
- Best Practices: 90%
- SEO: 90%
While this is a personal portfolio, bug reports and suggestions are welcome. Please open an issue to discuss any changes.
This project uses Conventional Commits:
feat: new feature
fix: bug fix
refactor: code refactoring
chore: maintenance tasks
perf: performance improvements
docs: documentation changes
style: formatting changes
This project is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
You are free to:
- Share -- copy and redistribute the material in any medium or format
Under the following terms:
- Attribution -- You must give appropriate credit, provide a link to the license, and indicate if changes were made
- NonCommercial -- You may not use the material for commercial purposes
- NoDerivatives -- If you remix, transform, or build upon the material, you may not distribute the modified material
