Skip to content

Repository files navigation

@albertolicea00's Portfolio

GitHub Pages Cloudflare Pages Vercel Render Netlify

Note: The redirect branch (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/.

DuckDuckGo Google Brave Bing Yahoo Yandex Baidu

✨ Features

  • 🌓 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

🛠️ Tech Stack

  • 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

⚖️ Trade-offs (why there's no framework)

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.html and projects.html duplicate 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-server full-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.

📁 Project Structure

├── 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.md

📝 Content Model

Each file in assets/i18n/ contains:

  • home: UI text, labels, accessibility copy, and section content
  • projects: Project cards rendered on the home page and projects page
  • experience: 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.

🔄 Updating Content

  1. Edit assets/i18n/en.json to update the default English content.
  2. Mirror those changes in the other language files (es.json, etc.) if you want localized versions.
  3. Add or update entries in the projects array to change the portfolio cards.
  4. Add or update entries in the experience array to change the timeline.
  5. Replace assets in assets/img/, assets/icons/, or assets/pdf/ when needed.

💻 Local Preview

Open index.html directly in the browser for a quick check, or serve the folder with auto-reload:

make dev

This runs live-server and opens the browser automatically.

To test the full API and contact form locally, run the included server:

make start

Then visit http://localhost:3000.

Run the Python test suite with:

make test

Or 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.

🚀 Deployment

This project can be deployed anywhere. It supports a Multi-Cloud Zero-Config architecture, meaning it runs seamlessly on:

🪄 The Multi-Cloud Architecture Trick

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, the Makefile includes a build target (mkdir -p functions/api && cp api/contact.mjs functions/api/contact.mjs). By setting your Cloudflare Pages build command to make build, Cloudflare dynamically creates the required folder structure during deployment.
  • Netlify: functions/netlify/contact.mjs is a thin wrapper that imports processRequest from the master file and exposes it as a classic (event, context) handler. It has to live in its own file with no export 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 named handler export. netlify.toml points functions at that directory and rewrites /api/* to /.netlify/functions/:splat.
  • Render / Coolify / Dokploy: The included server.mjs natively imports the master file and serves it as a standard Node.js Express-like endpoint.

🔑 Contact Form Environment Variables

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 @userinfobot to 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!)

🏢 Self-Hosted PaaS (Coolify, Dokploy) & Container Platforms (Render, Heroku)

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).