English · Italiano
Live at devmanfre.netlify.app — a single-page, bilingual (English/Italian), dark-first portfolio and CV site built with Next.js.
The Site
Development
Management & Operations
- Managing Content
- Internationalization (i18n)
- Analytics
- External Services
- Deployment & CI
- Git Workflow
- AI-Assisted Maintenance
- License
- Single page, fully bilingual — path-based locales (
/en,/it) powered by next-intl, withx-defaultfallback to English. Every user-facing string exists in both languages. - Dark theme by default — light/dark switch (top-right toggle) via next-themes; theme tokens are CSS variables in
globals.cssmapped to semantic Tailwind colors. - Animated "letter glitch" background — a canvas matrix-style effect (ReactBits
LetterGlitch) that pauses when off-screen or when the tab is hidden, and respectsprefers-reduced-motion. - Hero — name, tagline, avatar, one-click CV download (locale-specific PDF) and a social icon row (GitHub, LinkedIn, CodePen, email) with tooltips.
- CV sections — About, Work Experience, Education, Skills, Languages, Certifications, all rendered from a single typed data object (
src/data/resume.tsx) with staggered blur-fade entrance animations (Magic UI). - Interactive terminal — a fake shell with real commands and hidden easter eggs. See below.
- GitHub Activity section — server-rendered stats fetched from the GitHub GraphQL API: public repo count, total stars, contributions in the last year, top language, a contribution calendar heatmap, and up to 4 pinned repositories. Data is cached for 24 hours (
revalidate: 86400) with a 5-second fetch timeout. The whole section silently disappears ifGITHUB_TOKENis missing or any API call fails — the build never breaks because of it. - CodePen Experiments section — hand-curated pens embedded as lazy-loaded iframes whose embed theme follows the site theme. Curated by hand because CodePen has no public API (its RSS feed is Cloudflare-blocked); the section hides itself when the pen list is empty.
- Projects grid — cards with image/video, tech badges, and per-project links (GitHub, articles, live demos).
- Contact section — direct LinkedIn and email call-to-action buttons.
- Animated 404 page — "404 / Page not found" rendered with the ReactBits
TrueFocusfocus-shift animation. - SEO — per-locale metadata and Open Graph tags, generated OG image,
hreflangalternates, JSON-LDPersonschema (withsameAssocial links),sitemap.xml, androbots.txt. - Privacy-friendly analytics — Umami with custom events, loaded only when configured (details).
- Accessibility touches —
aria-liveterminal log,aria-labels on icon-only controls, reduced-motion support, keyboard-friendly terminal input.
The Education section ends with a terminal (src/components/interactive-terminal.tsx). On load it plays a scripted typing intro (defined in DATA.terminal):
> get-graduation --spec IT
✔ IT graduation getted.
> get-degree --spec CS
✔ CS degree getted.
> sudo work --mode hard ...
After the intro (~intro length + 500 ms) an interactive prompt appears: visitor@devmanfre:~$. Commands are case-insensitive; history is capped at 50 entries. Typing clear also removes the intro.
| Command | Effect |
|---|---|
help |
Lists the commands below |
whoami |
Name and role |
skills |
Tech skills from DATA.skills |
languages |
Spoken languages with levels |
certs |
Certifications |
projects |
Project names with links |
contact |
Clickable email / LinkedIn / GitHub links |
cv |
Downloads the CV PDF for the current locale |
clear |
Clears history and the intro |
Undocumented commands — click to reveal
| Command | Effect |
|---|---|
sudo matrix |
The big one. Prints "Wake up, Neo…", dispatches a matrix-burst browser event, and the background LetterGlitch canvas goes fullscreen for 5 seconds before fading out over 0.7 s. Skipped entirely under prefers-reduced-motion. Tracked as the matrix-egg analytics event. |
sudo <anything else> |
"visitor is not in the sudoers file. This incident will be reported." |
rm -rf / |
"nope. this portfolio is read-only :)" |
exit |
"nice try. you can check out any time you like, but you can never leave." (Hotel California) |
ls |
"just a portfolio here — try help" |
pwd |
/home/visitor/devmanfre |
| anything else | command not found: <cmd> — try 'help' |
The matrix-burst event is a window-level CustomEvent contract between the terminal (dispatcher) and LetterGlitch (listener) — no shared state, so either component works without the other.
| Layer | Choice |
|---|---|
| Framework | Next.js 15 (App Router, Turbopack dev server) |
| UI library | React 19 — Server Components by default, "use client" only where needed |
| Language | TypeScript (strict) |
| Styling | Tailwind CSS 3.4 + CSS-variable theme tokens |
| UI primitives | shadcn/ui (Radix UI) — managed via the shadcn CLI, not hand-edited |
| Animations | Motion / Framer Motion, vendored Magic UI & ReactBits components (the latter via jsrepo, see jsrepo.json) |
| i18n | next-intl 4 |
| Theming | next-themes |
| Hosting | Netlify |
| Analytics | Umami Cloud (optional) |
src/
├── app/
│ ├── [locale]/ # localized routes (en / it)
│ │ ├── layout.tsx # JSON-LD, glitch background, theme toggle
│ │ ├── page.tsx # THE page — all sections live here
│ │ └── opengraph-image.tsx
│ ├── layout.tsx # root: metadata, ThemeProvider, next-intl, Umami
│ ├── not-found.tsx # animated 404
│ ├── globals.css # theme tokens (CSS variables)
│ ├── robots.ts / sitemap.ts
├── components/
│ ├── ui/ # shadcn/ui primitives (CLI-managed — don't hand-edit)
│ ├── magicui/ # vendored animation components (terminal, blur-fade)
│ ├── reactbits/ # vendored via jsrepo (LetterGlitch, TrueFocus)
│ └── *.tsx # custom components (interactive-terminal,
│ # github-section, codepen-*, project-card, …)
├── data/
│ └── resume.tsx # ⭐ single source of truth for ALL content
├── i18n/ # next-intl routing / request config
├── lib/ # pure modules, unit-tested (vitest)
│ ├── utils.ts # cn() + formatDate()/formatPeriod()
│ ├── terminal/commands.ts # command registry (thin component adapter)
│ ├── analytics.ts # typed Umami event catalog (trackEvent, eventAttrs)
│ ├── matrix-burst.ts # owns the matrix-burst window event
│ ├── github.ts # GitHub fetcher (disabled | error | ok status)
│ ├── github-stats.ts # pure GitHub stats aggregations
│ └── section-sequence.ts # page section order (drives animation delays)
└── middleware.ts # locale-detection middleware
public/
├── locales/en.json, it.json # UI chrome strings (key sets must match)
├── resumes/resume-{en,it}.pdf # downloadable CVs (exported externally)
└── img/ # avatar, work/education logos, project screenshots
Requires Node.js 20+ and npm.
git clone https://github.com/DevManfre/web-portfolio.git
cd web-portfolio
npm install
npm run dev # dev server with Turbopack → http://localhost:3000| Script | Purpose |
|---|---|
npm run dev |
Development server (Turbopack) |
npm run build |
Production build |
npm run start |
Serve the production build |
npm run lint |
ESLint (next/core-web-vitals + next/typescript) |
npm run typecheck |
tsc --noEmit |
npm test |
Vitest — unit tests for the pure modules in src/lib/ and the burst state machine |
Unit tests (Vitest) cover the pure modules only (terminal commands, analytics catalog, burst state machine, GitHub stats, section sequence, date formatting). No component/E2E tests. Lint + typecheck + test + build in CI are the quality gate.
Both variables are optional — the site builds and runs without them; the related features simply switch off.
| Variable | Where | Purpose |
|---|---|---|
GITHUB_TOKEN |
.env.local locally, Netlify dashboard in production |
Classic PAT with the read:user scope. Enables the GitHub Activity section (GraphQL API). Missing/invalid → section silently hidden. CI builds without it by design. |
NEXT_PUBLIC_UMAMI_WEBSITE_ID |
Netlify dashboard (optionally .env.local to test) |
Umami website ID. When unset the analytics <script> is simply not rendered — no tracking in dev/CI. |
Almost everything you see on the page comes from src/data/resume.tsx (the DATA const, typed as const):
| What | Where | Notes |
|---|---|---|
| Name, tagline, summary, avatar | DATA.name/description/summary/avatarUrl |
Long-form fields are { en, it } objects |
| Terminal intro script | DATA.terminal |
Alternating lines: even indexes are typed, odd indexes appear as green output |
| Skills | DATA.skills |
Auto-sorted alphabetically |
| Socials & contact | DATA.contact |
Feeds hero icons, terminal contact, JSON-LD sameAs |
| Work / education / certifications | DATA.work/education/certifications |
Logos in public/img/… (webp) |
| Spoken languages | DATA.languages |
Bilingual name + level |
| Projects | DATA.projects |
Image in public/img/projects/, tech badges, links array |
| CodePen pens | DATA.codepens |
{ title, slug } — the slug is the last path segment of codepen.io/devmanfre/pen/<slug>. Hand-curated (no public CodePen API). |
| UI chrome strings | public/locales/en.json + it.json |
next-intl messages — key sets must stay identical |
After changing CV data, regenerate public/resumes/resume-en.pdf and resume-it.pdf — they are exported externally, not generated by the build.
Repo-specific Claude Code skills automate the common edits — see AI-Assisted Maintenance.
- Locales:
en(default) andit, path-prefixed (/en,/it); middleware redirects/byAccept-Language. - Rule: every user-facing string must exist in both languages — content as
{ en, it }objects inresume.tsx, UI strings as the same key in both locale JSON files. - SEO follows along:
hreflangalternates, per-locale OG metadata (en_US/it_IT), both locales in the sitemap.
Umami (cookieless, GDPR-friendly) loads only when NEXT_PUBLIC_UMAMI_WEBSITE_ID is set. Custom events:
| Event | Fired by |
|---|---|
cv-download (source: hero | terminal) |
CV button in the hero / cv terminal command |
social-github / social-linkedin / social-codepen / social-email |
Hero social icon row |
contact-linkedin / contact-email |
Contact section buttons |
codepen-view-profile |
CodePen profile link |
theme-toggle |
Light/dark switch |
terminal-used |
First terminal command of a visit |
matrix-egg |
sudo matrix easter egg |
Static elements use data-umami-event attributes; programmatic events go through the safe trackEvent() wrapper in src/lib/analytics.ts (no-op when Umami is absent).
| Service | Role | Config lives in |
|---|---|---|
| Netlify | Hosting + deploys of production (site) — deploy config only in the Netlify dashboard, nothing committed |
Netlify dashboard |
| GitHub GraphQL API | Data for the GitHub Activity section | GITHUB_TOKEN env var |
| Umami Cloud | Analytics (cloud.umami.is/script.js) |
NEXT_PUBLIC_UMAMI_WEBSITE_ID env var |
| CodePen | Embedded pen iframes (codepen.io/<user>/embed/preview/<slug>) |
DATA.codepens |
| GitHub Actions | CI quality gate | .github/workflows/ci.yml |
| capsule-render / shields.io | README header art and badges | this file |
- Branches: work happens on
development;productionis the release branch, updated only by mergingdevelopmentinto it. Netlify deploys the site fromproduction. - CI (GitHub Actions): on every push/PR to
developmentorproduction—npm ci→lint→typecheck→test→build(Node 20). ⚠️ ESLint errors are ignored during builds (ignoreDuringBuilds: trueinnext.config.ts), so CI lint is the real gate — keep it green.
Commit messages use a Unicode gitmoji prefix + a short English message:
| Emoji | Meaning |
|---|---|
| ✨ | feature |
| 🔧 | config |
| 🗃️ | data (resume.tsx) |
| 💄 | UI / style |
| 🌐 | i18n |
| 🐛 | fix |
| ♻️ | refactor |
| 📝 | docs |
| 🚧 | WIP |
The repo ships with a CLAUDE.md playbook and project-scoped Claude Code skills in .claude/skills/:
| Skill | Use |
|---|---|
add-project |
Add a project to the portfolio |
update-cv |
Update work experience, education, or skills |
update-resume-data |
Edit any other DATA content (bio, socials, terminal intro, …) |
update-locales |
Add/edit/remove UI strings in both locale files |
i18n-check |
Audit English/Italian parity |
update-readme |
Keep this README and README.it.md in sync with the code |
release |
Pre-flight checks, then merge development → production |
MIT © Alessio Manfredini