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
├── 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 (HTML only — no inline scripts)
│ ├── script.js # All bento card logic (external, browser-cacheable)
│ ├── style.css # Bento-specific styles
│ └── assets/ # Brand SVGs and .webp images for bento cards
│
├── 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 pages including/bento/. Do not add a secondary sitemap 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. The HTML is markup-only; all logic lives in the external bento/script.js (browser-cacheable, no inline <script> block). 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
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.
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 |
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">.
| 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 |
| 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 |
|---|---|
| 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 nirzak-streak-stats.vercel.app |
| 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 |
| Security | All dynamically created <a target="_blank"> elements have rel="noopener noreferrer" set via card.rel in JS |
| Feature | How it works |
|---|---|
| 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 |
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. Cloudflare automatically rewrites mailto: links and email addresses in the HTML, injecting /cdn-cgi/scripts/email-decode.min.js to decode them client-side. Spam bots scraping the raw HTML will not see the real addresses. This only works when traffic is proxied through Cloudflare (🟠 orange cloud DNS).
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>. - Copy the
.site-navblock from an existing page; add a nav link to it across all pages. - 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.