Rebuilding My Developer Portfolio
A measured rebuild of rivernguyen.id.vn — Next.js App Router, bilingual routing, NDA-safe case studies, a keyless portfolio bot, and the Core Web Vitals tradeoffs I actually hit.
Nguyễn Đình Giang
· · 5 min read
Frontend developer in Ha Noi. I write about the Next.js, WordPress, and performance work I actually ship.
After shipping client frontends for a couple of years, I finally rebuilt my own site from scratch. Not a theme swap — a rethink of how I want recruiters, clients, and (increasingly) AI crawlers to understand who I am and what I ship.
This post is the first-person case study for rivernguyen.id.vn: stack choices, bilingual routing, anonymized case studies, a local “River Bot,” and the lab numbers that still need work.
Why rebuild at all?
My old portfolio did the basics — name, contact, a flat project list. It did not reflect how I work today: Next.js App Router, WordPress as a headless CMS on client work, GSAP / Motion for intentional motion, and a bias toward performance and SEO.
I wanted a site that:
- Loads reasonably fast and can be measured honestly with Lighthouse / CrUX later
- Supports English + Vietnamese without duplicate-content traps
- Lets me publish MDX blog posts and project case studies from data modules
- Feels polished without turning into a dashboard of cards
The bar was simple: if I removed the nav, the first viewport should still read as my brand — not a generic “frontend developer” template.
Starting point: fork, then own the brand
The early codebase borrowed structure from a strong open-source portfolio shell (component registry patterns, MDX blog plumbing, screen-line layout). That was intentional — I wanted proven App Router patterns, not a blank Vite SPA.
The rebuild work was about ownership:
- River identity (name variants, Ha Noi timezone, affiliate badge, mascot)
- Case studies that respect NDAs without looking empty
- Bilingual copy that is actually translated, not machine-dumped
- Interaction that matches my craft (mascot, duck follower, command menu) without blocking LCP
Tech choices that stuck
- Next.js App Router — routing, metadata APIs, static generation for blog/case studies
- Tailwind CSS + shadcn/ui — consistent primitives without inventing a design system twice
- next-intl —
as-neededlocale prefix (/for EN,/vi/...for Vietnamese) - MDX via fumadocs-style helpers — posts as files, not a CMS for a solo site
- Upstash Redis (with a local JSON fallback) — visitors insights without standing up a database
- GSAP / Motion — motion where it earns its place; still a CWV cost on mobile if loaded eagerly
Docker packaging for this site is covered separately in Shipping Next.js with Docker without leaking secrets.
Bilingual routing without shooting myself in the foot
Vietnamese is not a “nice to have” for me — it is half the audience. The hard part is not translating strings; it is canonical + hreflang consistency.
Lessons that mattered:
createPageMetadata()must resolve the request locale intoalternates.canonicalandopenGraph.url. Hardcoding the default locale makes/vi/*look like duplicates of English.- Sitemap entries should include both locales only after canonicals are correct.
- Page bodies can be fully translated while titles/descriptions stay English by accident — that still weakens Vietnamese SERPs.
I wrote the routing patterns up in Building bilingual Next.js sites with next-intl.
Case studies under NDA
Most of my shipped work is client-owned. Showing live URLs and branded screenshots is often not allowed. Recruiters still need proof of craft.
The compromise on this site:
- Generic titles (project type + domain), no client names
- Wireframe-style anonymized UI mocks (filters, auth shell, locale toggle) instead of stolen screenshots
- Architecture flows as diagrams, not branded system maps
- Lab metrics (LCP / Performance) stated as lab numbers, not marketing fiction
- A clear private-project notice with a path to /contact
That is not as strong as a public case study with a live URL — and I say so on the page. Honesty beats fake logos.
Browse the structure at /projects.
River Bot without an API key
I originally wired an AI SDK chat against OpenAI. For a personal portfolio, that meant keys, rate limits, and a failure mode that just returned 503.
The version that shipped is a local FAQ / intent bot: keyword scoring over projects, stack, experience, hire, and contact copy pulled from the same data modules the UI uses. It streams plain text, answers EN/VI, and refuses off-topic prompts.
Tradeoff: it will not invent novel answers. Benefit: zero vendor lock-in, zero leaked keys, and the bot stays on-brand.
Personality: mascot, duck, pixel mode
Portfolios that only list skills are forgettable. I added:
- A cursor-tracking mascot on the profile header
- An optional duck follower (desktop, motion-safe)
- A Konami unlock for pixel mode
- A tiny secret /pond room if you keep poking the duck
These are deliberate brand signals — and also JS/CSS cost. On mobile lab runs they compete with analytics and motion for main-thread time. I treat them as progressive enhancement, not the content.
What the lab numbers said (honestly)
A full SEO/performance audit of this domain (lab Lighthouse, no CrUX credentials configured yet) flagged the homepage mobile score around 44/100, with LCP ~5.1s and high TBT — driven largely by analytics SDK weight and motion chunks on the critical path. Desktop was fine (~90).
That is an awkward sentence to publish on my own site. It is also the point of a rebuild journal: measure, then cut.
Remediation direction (tracked as ongoing work):
- Defer / tree-shake analytics modules that are not used (surveys, toolbars)
- Code-split motion off the first paint
- Stop over-preloading decorative fonts
- Serve the avatar at display size, not 400×400 for a ~120px box
Deeper CWV notes: Optimizing Next.js for Core Web Vitals.
Recruiter mode
Not every visitor wants the full scroll. /hire (also /?mode=hire) is a one-screen snapshot: availability, stack, three case studies, CTAs to contact and resume. The command menu jumps there quickly.
What I would do differently next time
- Ship visual proof earlier — anonymized mocks should have landed with the first case study, not after an audit called out “text-only projects.”
- Treat locale metadata as a feature, not a helper afterthought.
- Budget JS for personality the same way I budget images — mascot joy is real; so is TBT.
- Write fewer, deeper posts instead of thin stubs that cannot rank or get cited.
What’s next
I will keep this site as a living lab: deepen a few technical posts, keep case studies NDA-honest, and chip away at the mobile CWV gap until field data (CrUX) can replace lab-only numbers.
If you are rebuilding your own portfolio, start with one sharp case study and correct canonicals — not another hero animation. Or get in touch if you want to talk through a Next.js / WordPress frontend rebuild.
Stack references: Next.js App Router, next-intl, Tailwind CSS, web.dev vitals.