Note: The
redirectbranch (https://github.com/albertolicea00/portfolio/tree/redirect) holds a zero-config static redirect to the Target (Vercel) deployment site, for hosts where you don't want to manage env vars/functions.
Responsive portfolio built with plain HTML, CSS, and JavaScript. The site ships as a pure static frontend with no build step and loads its UI copy and dynamic sections from per-language JSON files in assets/i18n/.
- 🌓 Theme Toggle: Light and dark mode support
- 🌍 Localization: Multi-language interface with automatic browser detection and English fallback
- ⌨️ Accessibility: Custom language dropdown with keyboard support
- 🗂️ Dynamic Content: Project and experience sections rendered dynamically from JSON
- 📱 Responsive: Fluid layout optimized for desktop, tablet, and mobile
- ✉️ Secure Contact Form: Powered by Serverless Functions and Telegram Bot API
- 🛡️ Spam Protection: Built-in Honeypot and Cloudflare Turnstile verification
- 🎨 Premium Assets: Local SVG icons for high-quality, crisp rendering (emoji-free)
- ⚡ Zero Dependencies: No heavy frontend frameworks or runtime dependencies
- Frontend: HTML5, CSS3, JavaScript (ES6+)
- Backend (Serverless): Node.js (Vercel Functions, Cloudflare Pages, Netlify)
- Messaging: Telegram Bot API
- Testing: Python (pytest)
- CI/CD: GitHub Actions
- Typography: Google Fonts
Tracked in #6.
This portfolio is deliberately vanilla — HTML/CSS/JS, no build step, no React/Vue/Svelte, no bundler. Reason: personal repo, built raw on purpose to showcase skills. No regrets, just not general advice. Costs:
- No componentization:
index.htmlandprojects.htmlduplicate markup (header, nav, footer, cards). Structural changes get hand-copied everywhere instead of living in one shared component. - Weaker SEO: content (projects, experience, i18n copy) isn't in the initial HTML — it's fetched from
assets/i18n/JSON at runtime. Crawlers that don't execute JS, or hit it under a tight budget, can index an empty page. SSG/SSR (Next.js, Astro, Nuxt) ships populated HTML on first byte. - No reactivity/state: theme toggle, language dropdown, card rendering — all manual DOM manipulation. Imperative, easy to desync (e.g. update a class but forget the matching
aria-*). - No type safety: plain JS. Typos/shape errors in
assets/i18n/JSON surface at runtime, not compile time. - No build tooling: no tree-shaking, code-splitting, minification. Fine at this size, won't scale.
- Testing covers structure, not behavior: Python suite checks files/assets/links, not UI/JS logic.
- Weaker DX:
live-serverfull-reloads, no HMR, no linting/type-checking on save.
Fine for a small personal portfolio. For anything meant to grow — more pages, more contributors, content that needs to rank — use a framework with SSR/SSG from day one.
Update: this was fully explored on the astro-migration branch (tracked in #12) — a working end-to-end Astro rewrite that fixes every cost listed above. It works, and #12 was closed as not planned: staying vanilla for this portfolio. Keeping this issue open and pinned as the living record of that trade-off.
├── index.html
├── projects.html
├── style.css
├── script.js
├── server.mjs # Local dev server + Node backend for Render/Coolify/Dokploy
├── Makefile # build, start, dev, test, and helper-script targets
├── functions/
│ ├── api/ # Build output for Cloudflare Pages (`make build` copies contact.mjs here)
│ └── netlify/ # Netlify Functions wrapper around contact.mjs
├── api/
│ └── contact.mjs # Master serverless handler, reused by every deployment target
├── assets/
│ ├── i18n/ # Per-language JSON content (en, es, de, fr, it, ja, ko, pt, ru, zh)
│ ├── icons/
│ │ ├── tech/ # Local tech-stack SVG icons
│ │ ├── social/ # Social platform icons + social-sprite.svg
│ │ ├── flags/ # Language-switcher flag icons
│ │ └── emojis/ # Local emoji replacements (SVG)
│ ├── img/ # avatar, hero background, apps/ and company/ screenshots
│ └── pdf/ # CV....pdf
├── scripts/ # Standalone helper scripts (not shipped with the site)
│ ├── fetch_icons.py # Downloads missing tech icons via simple-icons
│ ├── validate_translations.py # Audits assets/i18n/ for missing/duplicate/untranslated keys
│ └── package.json # simple-icons dependency used by fetch_icons.py
├── tests/ # Python test suite
│ ├── run_all.py # Orchestrator (`make test`)
│ ├── test_structure.py
│ ├── test_assets.py
│ └── test_links.py
├── .github/workflows/test.yml # CI: runs the test suite on push/PR
├── netlify.toml # Netlify functions + /api/* redirect config
├── vercel.json # Vercel config
├── render.yaml # Render deployment config
├── robots.txt / sitemap.xml # SEO
├── llms.txt
├── LICENSE # MIT License
└── README.mdEach file in assets/i18n/ contains:
home: UI text, labels, accessibility copy, and section contentprojects: Project cards rendered on the home page and projects pageexperience: Timeline entries rendered dynamically
script.js loads assets/i18n/{lang}.json, applies translated UI strings, and falls back to en.json if a language file cannot be loaded.
- Edit
assets/i18n/en.jsonto update the default English content. - Mirror those changes in the other language files (
es.json, etc.) if you want localized versions. - Add or update entries in the
projectsarray to change the portfolio cards. - Add or update entries in the
experiencearray to change the timeline. - Replace assets in
assets/img/,assets/icons/, orassets/pdf/when needed.
Open index.html directly in the browser for a quick check, or serve the folder with auto-reload:
make devThis runs live-server and opens the browser automatically.
To test the full API and contact form locally, run the included server:
make startThen visit http://localhost:3000.
Run the Python test suite with:
make testOr run a single suite (make test-structure, make test-assets, make test-links). Helper scripts are available too: make fetch-icons and make validate-translations.
This project can be deployed anywhere. It supports a Multi-Cloud Zero-Config architecture, meaning it runs seamlessly on:
This portfolio uses a unique "Multi-Cloud Zero-Config" approach for its backend. Instead of duplicating backend code for every cloud provider, the entire API logic lives in a single master file: api/contact.mjs.
Here is how it seamlessly supports all major platforms with zero frontend code changes (the frontend simply calls /api/contact):
- Vercel: Natively looks for the
api/directory and exposes the file automatically. This is a true zero-config deployment. - Cloudflare Pages: Strictly requires a
functions/directory. To avoid code duplication, theMakefileincludes abuildtarget (mkdir -p functions/api && cp api/contact.mjs functions/api/contact.mjs). By setting your Cloudflare Pages build command tomake build, Cloudflare dynamically creates the required folder structure during deployment. - Netlify:
functions/netlify/contact.mjsis a thin wrapper that importsprocessRequestfrom the master file and exposes it as a classic (event,context) handler. It has to live in its own file with noexport default, because Netlify's function bundler treats any module with a default export as its newer Request/Response-style (v2) API and would otherwise route around the namedhandlerexport.netlify.tomlpointsfunctionsat that directory and rewrites/api/*to/.netlify/functions/:splat. - Render / Coolify / Dokploy: The included
server.mjsnatively imports the master file and serves it as a standard Node.js Express-like endpoint.
Regardless of where you deploy, the contact form requires the following environment variables:
TELEGRAM_BOT_TOKEN: Your Telegram Bot API token (from @BotFather).TELEGRAM_CHAT_ID: Your Telegram numeric Chat ID (use@userinfobotto find yours).TURNSTILE_SECRET_KEY: Your Cloudflare Turnstile Secret Key.
(Don't forget to add your Turnstile Site Key to index.html inside the <div class="cf-turnstile"> element!)
If you deploy this project to platforms like Coolify, Dokploy, Render, or Heroku, do NOT deploy it as a "Static Site". Instead, deploy it as a Node.js Web Service.
Thanks to the included server.mjs and Makefile (make start), these platforms will automatically start a native web server that serves both your static portfolio and the backend API on the same domain seamlessly.
(Note: If you deploy to GitHub Pages, the static site will work perfectly, but because GitHub Pages has no backend support, you must use an external form service like Formspree).