A modern and responsive portfolio website built with Astro and Tailwind CSS. Includes my projects, experience, skills, and more with an elegant, accessible, and multilingual interface.
- Multilingual: Spanish and English via Astro's native i18n routing (
/and/en/) - Accessibility: Font-size controls, accessible image-viewer dialog (focus trap,
aria-modal), andprefers-reduced-motionsupport - Animations: Smooth transitions, card animations, and typewriter effect
- Customizable Theme: Dynamic theme switching (light/dark) with localStorage persistence
- Responsive: Mobile-first design fully adaptable to all screen sizes
- Ultra Fast: Statically generated with Astro and WebP-optimized images
- Tailwind CSS: Modern and customizable styles with dark mode support
- Bento Grid Layout: Modern asymmetric grid design in About and Contact sections
- Material Icons: Consistent icon system using Google Material Icons
- Custom Scrollbar: Gradient-styled scrollbar that adapts to light/dark themes
- Animated Background: Wave animation effect in the hero section
- Link Previews: Rich hover previews for LinkedIn, GitHub, Artimark and projects
- Scroll Progress Indicator: Visual progress bar showing scroll position
- Schema.org: Structured data (JSON-LD) for improved SEO
- Skills Slider: Interactive slider with navigation controls for skills display
- KMP Terminal: Embedded Kotlin/Wasm terminal modal with boot/shutdown sequences and language sync
- Terminal Effects: Glitch animation on open, full-page vignette power-off on close
- Hardened Server: Custom Node server with security headers, CSP, rate limiting, and AI-crawler allowlist
- Custom 404: Real 404 page served with the correct HTTP status
- Crawler-friendly:
robots.txtandllms.txtfor search engines and LLMs
src/
├── components/
│ ├── Portfolio.astro # Composes the full page from sections
│ ├── layout/ # Header, Footer, AccessibilityControls
│ ├── sections/ # Hero, About, Education, Projects, Experience, Skills, Contact
│ └── ui/ # Reusable UI primitives (cards, buttons, tags, etc.)
├── config/
│ └── constants.ts # Shared constants (breakpoints, timings, zoom limits)
├── data/
│ ├── profile.ts # Single source of personal data + JSON-LD builder
│ ├── project-galleries.ts # Image galleries per project
│ ├── tech-logos.ts # Skill → logo mapping
│ └── link-previews.ts # Hover link-preview data
├── i18n/
│ ├── en.json # English translations
│ ├── es.json # Spanish translations
│ └── index.ts # i18n helpers (getTranslations, detectBrowserLanguage)
├── layouts/
│ └── Layout.astro # Main layout (meta, Schema.org, i18n data island)
├── pages/
│ ├── index.astro # Spanish homepage (default locale)
│ ├── en/index.astro # English homepage
│ └── 404.astro # Custom 404 page
├── scripts/ # Client TypeScript modules (bundled by Astro)
│ ├── image-viewer/ # Modular image viewer (modal, zoom, gestures, orchestrator)
│ ├── i18n-bootstrap.ts # Reads the i18n JSON data island into window.__i18n__
│ ├── theme.ts # Light/dark theme system
│ ├── terminal-modal.ts # KMP terminal modal (boot, shutdown, glitch, vignette)
│ ├── skills-slider.ts # Skills slider + progress bars
│ ├── scroll-ui.ts # Scroll progress bar + back-to-top (rAF throttled)
│ ├── scroll-animations.ts # IntersectionObserver fade-ins
│ ├── text-animations.ts # Section title / stagger animations
│ ├── accessibility.ts # Font-size controls + active nav section
│ ├── link-preview.ts # Rich hover previews
│ └── font-loader.ts # Non-blocking font loading (CSP-safe)
├── types/
│ └── translations.ts # TypeScript types for translations
├── assets/ # Images and resources
└── styles/
└── global.css # Global styles, scrollbar, reduced-motion
public/
├── CV/ # PDF documents (Spanish and English)
├── images/
│ ├── brand/ education/ experience/ hero/ projects/ skills/ # WebP assets
├── wasm/ # KMP terminal build output (copied from Terminal-KMP)
├── robots.txt # Crawler rules (incl. explicit AI bots)
└── llms.txt # Site summary for LLMs
server.js # Custom static file server (security headers, CSP, rate limit)
scripts/check-i18n-parity.js # Dev tool: verifies es.json / en.json key parity
Terminal-KMP/ # Kotlin/Wasm terminal (built into Portafolio-Astro/public/wasm/)
├── shared/ # Common KMP sources: data, i18n, command engine, Compose UI
├── webApp/ # WASM executable + copyWasmToAstro Gradle task
└── buildSrc/ # Custom Gradle plugins
| Command | Description |
|---|---|
npm install |
Install project dependencies |
npm run dev |
Start the development server at localhost:4321 |
npm run build |
Build the production site to ./dist/ |
npm run preview |
Preview the built site locally |
node server.js |
Serve the built ./dist/ in production (used on deploy) |
node scripts/check-i18n-parity.js |
Verify that es.json and en.json share the same keys |
| Command | Description |
|---|---|
./gradlew :webApp:copyWasmToAstro |
Build the KMP terminal and copy it to Portafolio-Astro/public/wasm/ |
./gradlew :shared:compileKotlinWasmJs |
Compile the shared KMP module for WASM |
./gradlew :webApp:wasmJsBrowserDevelopmentRun |
Run the terminal standalone in dev mode |
- Astro 6.4.8 - Modern SSG framework with native i18n routing
- Tailwind CSS 4.1.17 - Utility-first CSS framework
- Vite - Ultra-fast bundler
- TypeScript - Type-safe development (strict)
- Kotlin Multiplatform - Shared terminal logic and Compose UI
- Compose Multiplatform - Terminal UI compiled to Kotlin/Wasm
- Kotlin/Wasm - Terminal runtime embedded as a static WASM app
- Material Icons - Google's icon library
- Node.js - Custom static file server (
server.js) for production - Client scripts - Plain TypeScript modules bundled by Astro (no runtime dependencies)
Languages use Astro's native i18n routing: Spanish is the default at / and English lives at /en/. All text is rendered at build time from src/i18n/*.json. For the few client scripts that need translated strings, the active language is embedded as a non-executable JSON data island and read by i18n-bootstrap.ts into window.__i18n__.
Includes options to increase font size, improving the experience for all users. Controls are accessible via a dedicated component in the header.
- Card animations: Shimmer effect and hover transitions
- Typewriter effect: Dynamic text animation in the hero section
- Scroll animations: Fade-in effects when elements come into view
- Animated background: Wave animation in the hero section
- Terminal boot/shutdown: Typed terminal sequences on modal open/close
- Glitch effect: RGB channel split + scanlines when opening the terminal modal
- Vignette power-off: Full-page dark radial pulse after the terminal shuts down
A lightweight theme switching system (light/dark) stored in localStorage. The theme persists across sessions and adapts all UI elements including the custom scrollbar.
Modern asymmetric grid design used in:
- About section: Cards for professional description, languages, current status, continuous learning, and driver's license
- Contact section: Cards for email, phone, LinkedIn, GitHub, and address with different sizes for visual hierarchy
Structured data (JSON-LD) for improved SEO including:
- Person profile with job title
- Contact information
- Educational background (alumniOf)
- Skills and expertise (knowsAbout)
An embedded Kotlin/Wasm terminal accessible:
- Opens in a full-screen modal
- Boot sequence with typed system messages
- Shutdown sequence when closing
- Glitch opening effect with portfolio brand colors
- Full-page vignette "power-off" after shutdown
- Language sync between the Astro page and the terminal via
postMessage - Close button hidden during boot, shutdown, and glitch animations
Interactive slider with:
- Navigation buttons (prev/next)
- Technology logos from
/public/images/skills/ - Progress bars showing skill levels
- Responsive grid layout (1-3 columns)
A Kotlin Multiplatform module that compiles an interactive terminal UI to Kotlin/Wasm and embeds it into the Astro portfolio.
Terminal-KMP/
├── shared/
│ └── src/commonMain/kotlin/org/portafolio/terminalkmp/
│ ├── data/ # Portfolio data models (Lang, PortfolioData)
│ ├── i18n/
│ │ └── SystemStrings.kt # All terminal UI strings in ES/EN
│ └── terminal/
│ ├── engine/ # Command engine (DefaultCommands.kt, TerminalEngine.kt)
│ ├── ui/ # Compose terminal screen + state
│ └── engine/Span.kt # Terminal output styling
├── webApp/
│ ├── build.gradle.kts # Defines copyWasmToAstro task
│ └── src/wasmJsMain/kotlin/ # WASM-specific entry point + platform hooks
├── buildSrc/ # Custom Gradle plugins/tasks
└── nixpacks.toml (root) # Deployment build pipeline
- KMP sources live in
Terminal-KMP/ ./gradlew :webApp:copyWasmToAstrobuilds the terminal and copies the output toPortafolio-Astro/public/wasm/Portafolio-Astro/src/scripts/terminal-modal.tscreates the modal, loads/wasm/index.htmlin an iframe, and orchestrates boot/shutdown/glitch/vignette animations- Portfolio data is generated from
Portafolio-Astro/src/i18n/es.jsonanden.jsonvia thegeneratePortfolioDataGradle task - Language sync uses
postMessage(setLanguage) and alocalStoragefallback public/wasm/is ignored in git; Nixpacks rebuilds it on every deploy
After modifying the KMP terminal, run:
cd Terminal-KMP
./gradlew :webApp:copyWasmToAstroThen reload the Astro page. If you changed data in Portafolio-Astro/src/i18n/*.json, rebuild KMP so the generated data is refreshed.
Terminal strings are centralized in Terminal-KMP/shared/src/commonMain/kotlin/org/portafolio/terminalkmp/i18n/SystemStrings.kt:
ESandENconstructors hold every translatable string- Command descriptions, labels, CV prompts, and stack items all live there
- To add a new language or string, update
SystemStringsand usectx.stringsinside commands
Commands are implemented in Terminal-KMP/shared/src/commonMain/kotlin/org/portafolio/terminalkmp/terminal/engine/DefaultCommands.kt:
HelpCommand,AboutCommand,ProjectsCommand,ExperienceCommand,SkillsCommand,StackCommand,ContactCommand,LanguagesCommand,WhoamiCommand,CvCommand,OpenCommand,GalleryCommand,LangchangeCommand,ClearCommand,EchoCommand,BannerCommand- Each command implements
Commandand returnsCommandResult(lines, clear, newLang) TerminalEnginedispatches input, manages history, and providesCommandContextwith data, strings, and platform services
The production server (server.js) is a dependency-free Node static file server that adds:
- Security headers: CSP,
X-Frame-Options,X-Content-Type-Options, HSTS,Referrer-Policy,Permissions-Policy - Rate limiting: per-IP, with static assets and known AI/search crawlers exempted
- Path-traversal protection and a real 404 response for unknown routes
- Project images served as WebP (~87% smaller than the original PNGs)
- Fonts loaded non-blocking and subset to the weights actually used
- Scroll handlers throttled with
requestAnimationFrameand registered as passive - Animations respect
prefers-reduced-motion