- Next.js
- Design
Building nadeemashraf.cyou: a portfolio that reads like a build log
How I built a portfolio that works for a 30-second skim and a 10-minute read, on Next.js, Keystatic and Vercel, with the decisions behind each page.
Why a portfolio should tell a story
Most portfolios I looked at were lists: a grid of projects, a wall of logos, a contact form. They showed what someone had touched, not how they think.
Recruiters skim, so the site has to work at two speeds: a thirty-second scan for the facts, and a slower read for anyone who wants to see how I make decisions. That was the brief. The home page is the skim, and every section opens onto its own page for the slow read.
I'm both a software engineer and a forward deployed engineer, so the site has to say that honestly. The headline flips between the roles, and each role gets its own sentence. Only the forward deployed one talks about customer calls, because that's the role that takes them.
One page per section, and a light home
My first version put everything on the home page, with stacks of cards for work, projects, logs and skills. It was a lot to take in. I moved the detail out.
Home is now a hero and four short previews: Work, Projects, Build logs and About. Each preview is a few quiet rows that lead to a page with more content, and each page leads to the item itself. A reader decides how deep to go at every step.
Picking the stack
I wanted static pages wherever possible, because speed is the first thing a visitor notices. Next.js with the App Router gives me that. Pages render at build time, posts are MDX files in the repo, and Vercel deploys every push with its own preview link.
There is no database. Projects, posts and my profile live as MDX and YAML files in the repo, and I edit them through a small admin screen instead of touching components.
Everything visible on the site comes from that content. A single checkbox in my profile, "open to roles", hides the availability pill, the recruiter card and the pulse at once, because they all read the same field.
Text that fills as you read
Each preview on the home page opens with one sentence pinned on the left. As you scroll, its words turn from dim gray to white, one by one. It uses CSS scroll-driven animations, so there are no scroll listeners in JavaScript. If your system asks for reduced motion, the sentence is simply white from the start.
The rule I kept coming back to: the resting state is the finished page. Animation only ever starts from a dimmer state and returns to it, so a browser that can't animate shows everything, fully readable.
/* Each word fills across its own slice of the section's scroll */
.story { view-timeline-name: --sec; }
.story .w {
animation: fill linear both;
animation-timeline: --sec;
animation-range: cover calc(40vh + var(--p0) * 85vh)
cover calc(45vh + var(--p1) * 85vh);
}
@keyframes fill { from { color: var(--unread); } }
@media (prefers-reduced-motion: reduce) {
.story .w { animation: none; }
}Firefox has no scroll-driven animations yet. There, a tiny IntersectionObserver fallback loads only when the feature check fails and drives the same effect.
A headline that morphs
The hero headline changes with the role: forward deployed, full-stack, software. Each role has its own sentence, and they cross-dissolve through a blur into each other. All of the variants sit in one CSS grid cell, so the slot is always as tall as the tallest one and nothing around it moves. Under reduced motion, the first role simply stays.
Cards that stack, one section at a time
On the inner pages, cards stick and stack as you scroll, so a fast scroll can't skip past one before you've read it. The rule I settled on is that stacking belongs to a section. Each group of cards lives in its own container, so when a group ends, its cards release and the next group starts stacking from scratch. Anything that isn't a group of cards simply scrolls.
It's a plain CSS rule: position: sticky only holds inside its parent, so giving each group its own parent is the whole trick. It only turns on when the window is tall enough to show a whole card, and the first card in a group never fades in, so the page doesn't open on a blur.
Glass
The things that float above the page, such as the navigation bar, the sheets, the menu and the tooltips, are translucent with a backdrop blur. It keeps the page feeling like one surface and shows depth without adding colour. Where a browser doesn't support backdrop blur, they fall back to a solid surface.
This one bit me. For a while the blur silently didn't work in Chrome and Edge. I had written backdrop-filter and then -webkit-backdrop-filter with the same value, and the CSS minifier treated the second as a duplicate and kept only the prefixed one, which Chromium ignores. The fix was to write the prefixed one first and the standard one last. I only found it because I read the computed style in the browser instead of trusting the source.
Making it findable
Each page sets its own title and description through the Metadata API, the sitemap and robots files are generated at build time, and every post gets its own Open Graph image. The home page also describes me to search engines with structured data.
export function personJsonLd(p, socials) {
return {
'@context': 'https://schema.org',
'@type': 'Person',
name: p.name,
jobTitle: p.role,
url: 'https://nadeemashraf.cyou',
sameAs: socials.map((s) => s.url),
};
}For learning how people use the site, I wanted free tools only: Vercel's analytics and Speed Insights for visitors and performance, and PostHog's free tier for clicks and sections reached, running without cookies.
Rumi
There's a Siamese cat on this site. His name is Rumi, and he's written up as a character before he was drawn: a young adult who is curious, affectionate and independent, and who decides when. He comes over, naps near your cursor if you hold still, ignores a cotton ball or pounces on it, and walks away when he's had enough. He communicates with short sounds and body language, never sentences, and he never guilts you for leaving. Everything he does is his idea, not yours.
What I measured
I profiled the production build with Lighthouse (mobile, default throttling) and a scripted Edge session driven over the DevTools protocol. These are my numbers on my machine, so treat them as a baseline rather than a promise.
- 99-100Lighthouse performance on all seven page types, with CLS 0
- 0.8-1.0 slargest contentful paint, mobile emulation
- 0long tasks and zero frames over 50 ms while scrolling the home page
- Everything is static. The build prerenders 22 pages and there is no server runtime to wait on.
- First load is about 190 KB of JavaScript, gzipped. Roughly 70 KB is React DOM, 55 KB is the Next.js runtime, and 39 KB is my own code, which includes the sheets, the cat and the analytics wiring. CSS is a single 9.8 KB file and a page's HTML is 10 to 19 KB.
- Analytics don't count against that. PostHog is about 350 KB uncompressed, and it loads lazily after the page is interactive, so it isn't in the first load at all.
- Scrolling is cheap. Over a 4,800 px scroll, the page spent 25 ms in scripts and no time in layout. Frames stayed at 16.7 ms, the heap sat at 4.8 MB, and the DOM has about 1,300 nodes. That's because the text fill and the card reveals are scroll-driven CSS animations, so there is no scroll handler to run.
- Accessibility is 100 on every page except Home, which is 96. The gap is the dim words in the scroll fill, which are meant to read as unread. They're a transient state, and the resting text passes contrast.
- The cat is free until it moves. The purr and the meows are synthesized with the Web Audio API, so there are no audio files to download. The cat's sprite is an inline SVG built from a string grid.
The limits are real. The scroll test ran in headless Edge without a GPU and without CPU throttling, and I haven't tested Firefox, Safari or a real phone. Lighthouse's own mobile run is the closest I have to a slow device.
What I'd change
Honestly, not much. Writing the decisions down as I went meant I made them on purpose, and I'd make the same ones again: content as files, motion from CSS, and a resting state that's always readable. The thing I'll keep working on is the Firefox fallback, which uses a coarser version of the same effect until browsers catch up.
Need someone who owns a feature from the first call to the last deploy?
I'm open to senior software, full-stack and forward deployed roles, in India or abroad. A 15-minute call is the quickest way to see if we fit.