A static personal website hosted on GitHub Pages with Cloudflare as CDN and DNS provider. No build tools, no frameworks — just vanilla HTML, CSS, and JavaScript.
/
├── index.html # Home page
├── shared.css # Global design system
├── components.js # Shared nav & footer injector (loaded before i18n.js)
├── i18n.js # Internationalisation engine (EN/IT)
├── 404.html # Custom 404 error page
├── manifest.json # Web App Manifest (PWA metadata)
├── sitemap.xml # Single sitemap for all pages (covers / and /bento/)
├── robots.txt # Crawler rules
├── favicon.ico # Favicon (48×48)
├── CNAME # Custom domain binding for GitHub Pages
│
├── bento/
│ ├── index.html # Bento page — header fadeUp, parallax orbs, mouse tracking
│ ├── script.js # All bento card logic (async renderer, scroll reveal, GitHub fetch)
│ ├── style.css # Bento-specific styles (grid layout, card variants, responsive)
│ └── assets/ # Brand SVGs and .webp images for bento cards
│
├── cv/
│ └── index.html # CV page — embedded PDF viewer with download
│
├── contacts/
│ └── index.html # Contacts page (noindex — QR/NFC only)
│
└── assets/ # Global assets
├── logo.svg # Vector logo (used in navbar and footer)
├── favicon.png # Raster favicon (192×512px, used by manifest)
├── favicon.ico # Legacy favicon (48×48)
├── og-image.jpg # Open Graph preview image (1200×630, kept as JPG)
├── propic.webp # Profile picture (WebP)
└── ...
Note: There is no
bento/sitemap.xml. A singlesitemap.xmlat the root covers all indexed pages:/,/bento/, and/cv/. Do not add secondary sitemaps in subfolders.
Full-length scrollable portfolio page. Sections in order:
- Hero — name, role tag, description, CTA buttons
- About — photo, bio paragraphs, social links
- Projects — project cards (featured + regular); each card has interactive micro-animations
- Skills — grouped tech stack pills
- Experience & Education — timeline + edu grid
- CTA — contact call-to-action
- Footer
Grid of link cards. Card logic lives in bento/script.js (async renderer, scroll reveal observer, GitHub fetch). Card types:
github-custom— fetches live user data from the GitHub REST API (cached 1h inlocalStorage) and renders GitHub Streak Stats as an imagesolid— branded gradient card with SVG icon; supportsi18n_keyfor translated title/descinstagram-manual— 2×2 photo grid with overlay; images in.webpformat
Entry animation: the header uses staggered fadeUp (photo → label → h1 → bio → cta). Cards use the shared .reveal + IntersectionObserver system — the observer is only activated after all card DOM (including the async GitHub fetch) is fully painted, preventing a double-flash where the spinner would animate in first and the fetched content would pop in separately.
Dedicated page for the Curriculum Vitae.
- Embeds
assets/cvMatteoFontolan.pdfin a full-width<iframe>(80vh, responsive). - Two action buttons: Download CV (forces download via
downloadattribute) and Open in new tab (standard link). - Falls back gracefully if the browser does not support inline PDFs: a translated link is shown inside
<iframe>. - Fully i18n'd via
cv.*keys; the page title, subtitle, button labels, and footer link all switch language. - Linked from the home page hero button and the nav on all pages.
Minimal contact page designed for NFC/QR use cases (e.g. lost item tags).
- Not indexed (
<meta name="robots" content="noindex, nofollow">) — accessible only via QR/NFC link. - Features: WhatsApp deep-link with pre-filled message, Telegram link, tap-to-reveal phone number, vCard download.
- Entry animation: header uses staggered
fadeUp; action cards use.reveal+IntersectionObserverwith 100ms sibling stagger. Parallax orbs match homepage style.
Custom 404 page served automatically by GitHub Pages for any non-existent URL. Matches the site's visual style (animated orbs, gradient typography). Supports i18n via data-i18n attributes. Also excluded from indexing via noindex, nofollow.
All pages share a common set of CSS variables, base styles, and reusable components defined in shared.css.
| Variable | Purpose |
|---|---|
--bg-color |
Page background |
--card-bg |
Default card background |
--accent |
Primary accent colour |
--accent-bright |
Brighter accent for highlights and links |
--text-white/muted/bio/body |
Text colour hierarchy |
--radius / --radius-sm |
Border radius scale |
.site-nav— fixed top navbar with blur backdrop.card-base— base card style (border, hover lift, shimmer animation).card-slug— bottom-right URL label on cards.card-corner-icon— top-right icon on cards.btn-pill— rounded button; variants:.btn-primary,.btn-ghost.reveal— scroll-triggered fade-up animation (managed by IntersectionObserver in each page's script).site-footer— shared footer layout.profile-header— centred header with profile picture:focus-visible— keyboard navigation outline (accent colour)
All :hover transitions and card lift animations are wrapped inside:
@media (hover: hover) and (pointer: fine) { ... }This prevents the "sticky hover" bug on iOS and Android, where tapping a card leaves it permanently elevated or highlighted after the finger lifts. Touch devices don't have a hover state and must never trigger these styles.
The site auto-detects language from navigator.language and defaults to English for non-Italian browsers. The user's choice is persisted in localStorage under the key jw_lang.
i18n.jsis loaded in<head>(beforeDOMContentLoaded) on every page.- On
DOMContentLoaded, it injects a language toggle button into.nav-links, applies translations to the DOM, and setsdocument.documentElement.lang. - Static elements use
data-i18n="key"(plain text) ordata-i18n-html="key"(HTML content). - Dynamic card content (bento) uses
window.t('key')inside the card builder functions. - Clicking the toggle calls
window.toggleLang(), which re-applies translations and, on the bento page, clears and re-renders the entire grid. - Missing translation keys emit a
console.warn— the UI always falls back gracefully and never showsundefinedon screen.
Open i18n.js and edit the string inside the en or it object (or both). No other file needs to change.
// Example: update the hero description in English
'hero.desc': 'Your new description here.',Keys follow a section.element pattern:
| Prefix | Scope |
|---|---|
nav.* |
Navigation links |
hero.* |
Home hero section |
about.* |
About section |
projects.* / proj.* |
Projects section and individual project cards |
skills.* |
Skills section |
exp.* / edu.* |
Experience and education |
cta.* |
Call-to-action section |
home.footer_* |
Home footer |
bento.* |
Bento page header and footer |
card.* |
Bento card titles and descriptions |
contacts.* |
Contacts page |
cv.* |
CV page |
notfound.* |
404 page |
The following are hardcoded in HTML and must be edited there directly:
- Tech stack pills and project tags (
<span class="tag">) - Project year spans
- Proper names, social handles, email addresses
hrefattributes and external links- Decorative/structural elements with no text content
Every public page (index.html, bento/index.html) includes:
<meta name="description">— page description for search engines- Open Graph tags (
og:title,og:description,og:image,og:url) — controls link previews on WhatsApp, Telegram, LinkedIn, etc. - Twitter Card tags — controls previews on X/Twitter
<link rel="canonical">— prevents duplicate content issues- JSON-LD structured data (
@type: Person) on the home page — helps Google associate social profiles with the site
The contacts/ page and 404.html are intentionally excluded from indexing via <meta name="robots" content="noindex, nofollow">. The cv/ page is indexed and included in sitemap.xml.
| Asset | Format | Reason |
|---|---|---|
| Profile picture, project images | .webp |
Smallest size, broad browser support |
| Logo in navbar/footer | .svg |
Vector — pixel-perfect at any resolution/DPI |
og:image, apple-touch-icon |
.jpg / .png |
WhatsApp, Safari, and some crawlers reject WebP |
favicon.ico |
.ico |
Legacy browser compatibility |
Enables "Add to Home Screen" on mobile browsers. When installed:
- App name:
justwhitee — Matteo Fontolan - Short name:
justwhitee - Theme colour:
#00bbc9 - Icons:
/favicon.ico(48px) and/assets/favicon.png(192px, 512px)
All pages include <link rel="manifest" href="/manifest.json"> in <head>.
| Feature | How it works |
|---|---|
| Scroll reveal | IntersectionObserver on .reveal elements; bidirectional (fades out on scroll up) |
| Giant logo parallax | CSS transform on scroll via window.addEventListener('scroll') |
| Mouse parallax orbs | Two fixed .parallax-orb divs offset via mousemove |
| Nav active state | Section offsetTop tracking on scroll |
| Section nav (vertical dots) | Fixed right-side pill nav; active dot tracks whichever section centre is closest to viewport centre via scroll + requestAnimationFrame; hover expands pill with label; click flashes label for 600ms then collapses |
| Fan animation (RackController card) | requestAnimationFrame loop; speed increases on hover |
| Eye tracking (EdgeCV4Safety card) | SVG loaded via fetch; #pupil-focus-group translated on mousemove |
| Cookie crumbs (HashCrackerz card) | setInterval spawns absolutely-positioned div.crumb elements with CSS animation on hover |
| Feature | How it works |
|---|---|
| Header entry animation | Staggered fadeUp on profile pic, label, h1, bio, cta (delays 0.1s–0.7s) |
| Card scroll reveal | .reveal + IntersectionObserver; observer starts only after Promise.allSettled on all GitHub fetches + one requestAnimationFrame, so every card is fully rendered before any animation fires |
| Sibling stagger | Each card's transitionDelay is set to index × 60ms (capped at 320ms) at observe time |
| Mouse parallax orbs | Two fixed #orb1/#orb2 divs offset via mousemove + requestAnimationFrame (throttled) |
| GitHub live card | fetch to GitHub REST API (/users/:username) for profile data; result cached in localStorage for 1 hour (with try/catch for restrictive browsers) to avoid rate-limiting (HTTP 403 after 60 req/h); streak stats served as an image from streak-stats.demolab.com |
| i18n card titles | makeSolidCard and instagram cards read window.t('card.{i18n_key}.title') at render time; fallback to hardcoded item.title if key is missing, never undefined |
| Card shimmer | CSS ::after pseudo-element animation triggered on :hover (desktop only via @media (hover: hover)) |
| Language re-render | grid.innerHTML = "" + loadBento() called again on language toggle; reveal observer is reconnected after the new render completes |
| Security | All dynamically created <a target="_blank"> elements have rel="noopener noreferrer" set via card.rel in JS |
| Feature | How it works |
|---|---|
| Header entry animation | Staggered fadeUp (same pattern as bento) |
| Card scroll reveal | .reveal + IntersectionObserver with 100ms sibling stagger |
| Mouse parallax orbs | Same implementation as homepage and bento |
| Phone reveal | Toggling .hidden-info class between two .card-content views; card uses role="button" + tabindex="0" + onkeydown for full keyboard accessibility; attributes removed after unlock |
| vCard download | Programmatically creates a .vcf blob and triggers a download via a temporary <a> element |
| WhatsApp pre-fill | Message text is sourced from window.t('contacts.wa_msg') so it switches language with the toggle |
components.js lives at the site root and must be loaded before i18n.js on every page. It registers a DOMContentLoaded handler that injects the shared nav and footer into placeholder elements.
- Reads
document.currentScript.srcto computerootUrl— the absolute path to the site root. This makes all asset paths work underfile://, Live Server, and production without any hardcoded paths. - Builds the nav from a
NAV_LINKSarray and injects it into<nav id="site-nav" data-active="...">. - Builds a unified full footer (logo, name, copyright copy, GitHub/email/LinkedIn links) and injects it into
<footer id="site-footer" data-copy-key="...">. - Because it runs before
i18n.js's handler,.nav-linksalready exists wheni18n.jsappends the language toggle.
Each page needs two placeholder elements:
<nav id="site-nav" class="site-nav" data-active="bento"></nav>
...
<footer id="site-footer" class="site-footer" data-copy-key="bento.footer_copy"></footer>data-active controls which nav link gets the .active class. Valid values: home, bento, cv, contacts, "" (404).
Edit the NAV_LINKS array at the top of components.js — no HTML changes required across pages.
The footer email is a plain mailto: string inside components.js (not in static HTML). Cloudflare's email obfuscation only rewrites static HTML, so the address is never mangled during deployment.
All links with target="_blank" include rel="noopener noreferrer" — both in static HTML and in dynamically generated elements in script.js (card.rel = "noopener noreferrer"). This prevents third-party pages from accessing or hijacking the opener tab via window.opener.
Cloudflare's Scrape Shield / Email Obfuscation feature is enabled on the domain and rewrites mailto: links found in static HTML. To prevent mangling, the contact email in the footer is injected by components.js at runtime (not present in the raw HTML), so Cloudflare never sees it. If you add any mailto: links directly to HTML files, be aware they will be rewritten by Cloudflare on every deploy — either move them into JS or disable the feature for that page.
This project requires no build step, bundlers, or package managers.
git clone https://github.com/itsjustwhitee/your-repo-name.git
cd your-repo-nameImportant: Do not open
index.htmldirectly viafile://. The site usesfetch()for external resources (GitHub API, streak stats image), which triggers CORS errors under thefile://protocol. Always use a local web server. For instance:
- VS Code — install the Live Server extension and click "Go Live"
- Python — run
python3 -m http.server 8000and openhttp://localhost:8000
The site is hosted on GitHub Pages with Cloudflare as DNS provider, CDN, and security layer.
- Any
git pushto themainbranch triggers the default GitHub Pages workflow — changes go live in seconds. - The
CNAMEfile in the repository root binds the custom domain (justwhitee.org) to GitHub's servers. - GitHub Pages automatically serves
404.htmlfor any non-existent URL — no configuration required.
| Setting | Value | Notes |
|---|---|---|
| DNS | A records → GitHub Pages IPs (or CNAME → itsjustwhitee.github.io) |
Proxy enabled |
| SSL/TLS | Full | GitHub Pages provisions its own Let's Encrypt cert; Cloudflare ensures strict end-to-end encryption |
| Scrape Shield / Email Obfuscation | Enabled | Protects email addresses from spam bots (see Security section) |
| Caching | Default (CDN) | WebP images and static assets cached at edge globally |
- Create a new folder (e.g.
mypage/) with anindex.html. - Link
../shared.cssand../i18n.jsin<head>. - Add
<link rel="manifest" href="/manifest.json">and<meta name="theme-color" content="#00bbc9">in<head>. - Add
<nav id="site-nav" class="site-nav" data-active="mypage"></nav>where the navbar should appear. - Add
<footer id="site-footer" class="site-footer" data-copy-key="home.footer_copy"></footer>where the footer should appear. - Add the new page key to
NAV_LINKSincomponents.jsif it should appear in the navbar. - Add any page-specific translation keys to
i18n.jsunder a new prefix. - Add a
<style>block for page-specific CSS (or a separatestyle.cssin the folder). - Add the new URL to
sitemap.xmlin the root (unless the page should not be indexed). - Add
rel="noopener noreferrer"to anytarget="_blank"links.
- Add a new entry object to the
bentoDataarray inbento/script.js. - If the title or description should be translated, set
i18n_key: "yourkey"and addcard.yourkey.title(and optionallycard.yourkey.desc) to bothenanditini18n.js. The builder falls back toitem.titleif the key is missing — the UI will never break. - If it uses a brand gradient, add the colour pair to the
BRANDobject inbento/script.js. - Place any required SVG asset in
bento/assets/and any images as.webp.