Personal portfolio for Muhammad Abdullah, Security Operations Analyst. It's a single-page, static site designed in the style of apple.com, with light/dark themes and subtle scroll animations. Live at https://mabddullah.vercel.app.
| Layer | Choice |
|---|---|
| Build | Vite 8 |
| UI | React 19 + TypeScript |
| Styling | One plain stylesheet, src/styles/site.css (CSS custom properties) |
| Motion | CSS transitions + IntersectionObserver (no animation library) |
| Icons | lucide-react (GitHub and LinkedIn marks in brand-icons.ts) |
| Font | SF Pro via the system stack on Apple devices; self-hosted Inter elsewhere (@fontsource-variable/inter) |
| Hosting | Vercel (Git-connected, auto-deploy on push to main) |
There is no router and no backend: it's a static single page, pre-rendered to HTML at build time and hydrated in the browser. All content is data-driven from a single TypeScript file.
.
├── index.html # Head: meta, OG/Twitter, JSON-LD, favicon, FOUC script
├── public/ # Served at site root, copied verbatim into dist/
│ ├── favicon.png
│ ├── apple-touch-icon.png
│ ├── og.png # 1200×630 social share card
│ ├── robots.txt # Allows search + AI crawlers; points to sitemap
│ ├── sitemap.xml
│ └── llms.txt # Plain-text profile for LLM crawlers (AI readability)
├── src/
│ ├── main.tsx # Entry; hydrates the pre-rendered <App> (or renders /?print)
│ ├── entry-server.tsx # Build-time render of <App> to HTML
│ ├── app/
│ │ ├── App.tsx # Page composition: hero, about, experience, projects, skills, certs, contact
│ │ ├── lib/
│ │ │ ├── sections.ts # In-page nav items + resume PDF path (nav + footer)
│ │ │ ├── metrics.tsx # **metric** emphasis renderer, skills list splitter
│ │ │ ├── useThemeMode.ts # Light/dark: follows OS until toggled, then stored
│ │ │ └── motion.ts # prefers-reduced-motion helper
│ │ └── components/
│ │ ├── resume-data.ts # ⭐ ALL content + types (edit here)
│ │ ├── GlobalNav.tsx # Sticky translucent nav; full-screen menu ≤833px
│ │ ├── Hero.tsx # Photo, name, title, Download CV / Contact
│ │ ├── ExperienceTile.tsx # One card per role, with stats and buttons
│ │ ├── CountUp.tsx # Animated stat numbers
│ │ ├── DetailsModal.tsx # "Learn more" overlay (native <dialog>)
│ │ ├── Projects.tsx # Project cards (grid; swipe carousel on phones)
│ │ ├── BackToTop.tsx # Back-to-top button with reading-progress ring
│ │ ├── ThemeIcon.tsx # Sun/moon morphing icon
│ │ ├── SectionChips.tsx # Phone sticky section chips
│ │ ├── CommandPalette.tsx # ⌘K / Ctrl+K quick actions
│ │ ├── Toast.tsx # Confirmation toasts (aria-live)
│ │ ├── Footer.tsx # Directory columns + legal line
│ │ └── PrintResume.tsx # Optional print layout at /?print (data-driven)
│ ├── styles/
│ │ ├── site.css # All site styles + light/dark tokens
│ └── imports/
│ └── muhammad-abdullah.jpg # Profile photo (EXIF-stripped, 384px)
├── vercel.json # Framework, build, cache + security headers
├── vite.config.ts # Plugins + manual vendor chunk splitting
└── tsconfig.json
All resume content lives in
src/app/components/resume-data.ts:
profile, contacts, experience, projects, skills and certifications. The React app
imports and renders it; the components are generic and never hardcode copy.
Edit resume-data.ts directly. The types (ResumeEntry, Role, SkillGroup,
Certification, etc.) are defined at the top of the file and enforce the
structure of each entry: TypeScript will flag a malformed entry at build time
(npm run build).
Each experience entry supports optional bullets (string list),
sections (labeled paragraphs), roles (sub-positions), href (adds a
"View on GitHub" / "Visit website" button) and stats (big animated numbers on
the tile). Entries with sections, bullets or roles get a Learn more
button that opens the full detail in a modal. Text wrapped in
**double asterisks** in any body string (including profile.about) renders
emphasized.
A project can also carry an optional writeup (a ResumeEntry with sections).
Its card then gets a Read write-up button that opens it in the same modal.
statsshould only restate figures that already appear in that entry's copy.
npm install
npm run dev # http://localhost:5173npm run build # type-check, client build, server build, then scripts/prerender.mjs
# bakes the page HTML (and an Inter preload) into dist/index.html
npm run preview # serve the production build locallyRequires Node 20.19+ or 22.12+ (what Vite 8 needs; pinned via engines in package.json).
- Modelled on apple.com: values (type scale, colours, 44px blurred nav, pill
buttons, breakpoints 734 / 833 / 1068px) are taken from apple.com's own
stylesheets and documented at the top of
site.css. - Every CTA uses one button size (44px tall); buttons in a pair are equal width.
- Light/dark tokens live on
:root/:root[data-theme='dark']insite.css. The choice is stored inlocalStorage(theme-mode) and otherwise follows the OSprefers-color-scheme. - An inline script in
index.htmlsetsdata-themeand the background before React mounts, so dark-mode users never see a light flash (FOUC). - All content renders immediately (no fade-in or scroll-reveal). Motion only
decorates what is already on screen. Desktop: a ⌘K / Ctrl+K quick-actions
palette (jump to a section, download the CV, copy email with a toast, open
LinkedIn/GitHub, switch theme), a slight cursor tilt on cards and hover to
replay a stat's count-up. Everywhere: scroll-linked effects (cards and
headlines settle into place, the hero recedes, About text lights up word by
word as you read), count-up stats, a back-to-top button with a
reading-progress ring, a current-section marker (sliding underline in the
nav, a dot in the phone menu, sticky section chips on phones), a terminal-style
whoamiline in About, a cursor spotlight and lift on cards, a tap ripple and press-in on touch screens, a phone bottom-sheet for details (swipe down to close), a swipeable Projects carousel with page dots on phones, light haptic ticks on Android, a crossfade between themes (View Transitions, with a colour-fade fallback) with a sun-to-moon icon morph, and small button hover glows. Underprefers-reduced-motionall movement is off; the theme crossfade stays because a fade has no motion.
- The profile photo (
src/imports/muhammad-abdullah.jpg) is EXIF-stripped (GPS/device metadata removed) and resized to 384px: it displays at 144px in a circular avatar. To swap it, replace that file (keep it small; Vite hashes and emits it intodist/assets/). If the image is ever missing, the avatar falls back to theMAinitials monogram (seeHero.tsx). - Favicon, apple-touch-icon, and the OG image live in
public/(NOTdist/,dist/is wiped and rebuilt on every Vercel deploy). public/resume.pdfis the downloadable CV. It is generated from the/?printlayout (PrintResume.tsx), which reads the sameresume-data.tsas the site, so the two never disagree. The layout is a single column with standard headings so applicant tracking systems parse it in order. To regenerate after editing content:npm run build && npm run preview, openhttp://localhost:4173/?printin Chrome, Print, Save as PDF (paper size Letter, margins Default, headers and footers off), and replacepublic/resume.pdf.
Connected to this GitHub repo. Every push to main triggers a production build.
All config is in vercel.json:
- Framework
vite, buildnpm run build, outputdist - No rewrites: the site is a single page, so unknown paths return a real 404
(
public/404.html) Cache-Control: immutable(1 year) on hashed/assets/*- Security headers: a strict CSP (
script-src 'self', no inline scripts; the pre-paint theme script lives inpublic/theme-init.js),X-Frame-Options: DENY,X-Content-Type-Options,Referrer-Policy,Permissions-PolicyandCross-Origin-Opener-Policy. Vercel adds HSTS and the HTTP to HTTPS redirect. public/.well-known/security.txt(RFC 9116) lists the security contact. Update itsExpiresdate before 2027-09-28.SECURITY.mdpoints GitHub's Security tab to the same contact, and.github/dependabot.ymlopens weekly grouped dependency-update PRs.
Domain note: absolute URLs (OG image, canonical, JSON-LD, sitemap, robots, llms.txt, security.txt, the CV) point to the live domain
https://mabddullah.vercel.app. The originalmuhammad-abdullah-resume.vercel.apppermanently redirects (308) there. If a custom domain is added later, update those references acrossindex.htmlandpublic/(sitemap.xml,robots.txt,llms.txt).
- Meta: title, description, Open Graph + Twitter cards, canonical, theme-color.
- JSON-LD
Personschema inindex.html: gives search engines and AI crawlers structured facts (name, role, employer, skills, socials). robots.txtexplicitly allows search and AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended) and points to the sitemap.llms.txt: a plain-text profile summary for LLM crawlers, so AI answers about "Muhammad Abdullah" stay accurate.
To preview share cards: opengraph.xyz or metatags.io. Platforms cache OG data hard: use their debuggers (LinkedIn Post Inspector, Facebook Sharing Debugger) to force a re-scrape.