Back to projectsবাং / EN — one URL
Case study · Public figure

হাসনাত আবদুল্লাহHasnat Abdullah — Official Website

A bilingual digital home for a leader of Bangladesh's 2024 July Uprising. Not a campaign page: a Bangla-first, documentary-toned site that treats a national moment with editorial restraint — closer to a premium digital magazine than a political brochure.

Role
Design & development, solo — from PRD to production
Stack
Next.js 16 · React 19 · TypeScript · Tailwind CSS 4 · Framer Motion
Languages
বাংলা-first with English, one URL — no route splitting
Scope
8 routes, ~22 components, fully static and CMS-ready
Homepage hero in Bangla mode — the default voice of the site

The live homepage. Bangla is the primary voice; English is one toggle away — same URL, same markup.

8
routes, all server-rendered static pages
~22
components; only 8 ship client JavaScript
4
lines of CSS running the entire bilingual system
0
i18n libraries, state managers, or UI kits
1
URL per page — both languages, no route splitting
The brief

A public figure's site that couldn't look like one

Hasnat Abdullah was a coordinator of the Students Against Discrimination movement at the University of Dhaka and one of the most visible faces of the July Uprising — the 36-day mass movement of 2024 in which the UN estimates over 1,400 people lost their lives. His audience spans citizens, students, journalists, researchers, and international visitors.

The explicit constraint I set in the PRD: avoid everything political-party websites do — oversized portraits, heavy gradients, cluttered urgency. The uprising deserved a documentary register, not spectacle. And because most of the audience reads Bangla first, the site had to be genuinely Bangla-first, not an English site with a translation bolted on.

Bangla-first, truly

বাংলা is the default language, the primary typography, even the numerals. English is the alternate — not the other way around.

Documentary restraint

Memorial content is treated with quiet fields, hairlines, and photography — never sensationalism.

Static, but CMS-ready

Frontend-only by design. Typed content modules and JSON feeds are shaped so a CMS can slot in without a rewrite.

Fast on mobile networks

Built against Lighthouse 95+ targets across performance, accessibility, SEO, and best practices.

Visual identity

The flag, held quiet

The palette is the Bangladeshi flag decomposed and lowered in volume: the field green deepened into ink for memorial sections, the red circle reduced to a small crimson disc that recurs as the site's signature mark. Gold appears only in micro-labels; everything else rests on warm paper tones. Hind Siliguri carries the Bangla voice, Inter carries English — both self-hosted through next/font, so there are no external font requests and no layout shift.

Flag green
#006A4E
Primary — actions, links, theme color
Crimson
#F42A41
The signature mark — discs, rules, underbars
Ink
#0B2E24
Dark memorial fields
Gold
#C29A3B
Micro-labels, used sparingly
Paper
#FAF7F1
Warm background

The crimson disc

The flag's red circle, miniaturized. It bullets every eyebrow label and animates down a hairline as the hero's scroll indicator.

The fading rule

Section dividers taper from crimson to nothing — presence without weight.

River ripples

Faint horizontal lines over dark memorial fields, drawn from Bangladesh's rivers.

The underbar

Documentary photographs carry a crimson base rule, framing them as record rather than decoration.

The hero motto

তুমিও মানুষ আমিও মানুষ
তফাৎ শুধু শিরদাঁড়ায়

“You are human, so am I — the only difference is the spine.”

The interesting problem

Two languages, one URL, zero i18n libraries

Route-based i18n (/en, /bn) would have doubled every page and forced language state into the server. Instead, the entire bilingual system is four lines of CSS keyed off a single DOM attribute. A <T> component renders both languages into the server-rendered HTML as sibling spans; the html element carries data-lang, and CSS displays exactly one of the pair. Because the mechanism is pure CSS, every page — and even <T> itself — stays a server component. Only 8 of ~22 components ship client JavaScript.

The entire language engine
html[data-lang="bn"] [data-t][lang="en"] { display: none; }
html:not([data-lang="bn"]) [data-t][lang="bn"] { display: none; }

/* mode-scoped layouts */
html[data-lang="bn"] .only-en { display: none; }
html:not([data-lang="bn"]) .only-bn { display: none; }
lib/lang.ts — the DOM attribute is the store
export function setLang(lang: Lang) {
  document.documentElement.dataset.lang = lang;
  document.documentElement.lang = lang;
  localStorage.setItem("lang", lang);
  window.dispatchEvent(new Event("langchange"));
}

// no Context, no Redux, no i18n runtime
export const useLang = () =>
  useSyncExternalStore(subscribe, getLang, () => "bn");
01

No flash of the wrong language

A tiny inline script in <body> reads localStorage and stamps data-lang before first paint. The server renders Bangla; a returning English reader never sees it flicker.

02

Asymmetric layouts, single markup

The .only-bn / .only-en classes let one block compose differently per language — in the hero, the active language is the H1 while the other becomes a small subtitle beneath it.

03

Accessibility holds up

Hidden spans use display: none, so screen readers only ever hear the active language, and each span carries its correct lang attribute for pronunciation.

04

Even the numerals switch

Stat counters localize digits via toLocaleString("bn-BD") — ১,৪০০+ in Bangla mode, 1,400+ in English.

The memorial register

Restraint as a design position

The July Uprising memorial timeline — ink field, ripple texture, crimson discs marking dates

The July Uprising timeline: ink field, ripple texture, crimson discs marking dates, gold micro-labels.

Engineering details

Small decisions that add up

A counter that never misses

The stat counter is a hand-rolled requestAnimationFrame ease-out — no library. Three-way logic: already visible → animate now; scrolled past → snap to final; otherwise an IntersectionObserver waits. Reduced-motion users get the final value instantly.

Motion, twice guarded

One reusable Reveal primitive handles all scroll fades. prefers-reduced-motion is honored in both layers — useReducedMotion() in Framer Motion, and a CSS media query that kills the scroll-indicator animation.

YouTube without the API

Video feeds are plain JSON. A regex extracts the 11-char ID from watch, shorts, and youtu.be URLs; thumbnails use hqdefault.jpg — which always exists, unlike maxresdefault — cropped from 4:3 to 16:9 by object-cover.

Image discipline

Every photograph is hand-converted to WebP before shipping; originals stay out of the bundle. All rendering goes through next/image, and remotePatterns whitelists exactly one host: YouTube's thumbnail CDN.

SEO for two languages on one URL

Build-time OG image via next/og (English-only — the OG renderer lacks a Bangla font, and tofu would be worse). JSON-LD Person carries the Bangla name as alternateName; OG declares bn_BD with en_US alternate; the sitemap weights the memorial page 0.9.

Honest edges

The trades I made

The single-URL design means no hreflang pair for search engines — there's no second URL to point at. For this site the trade was right: shared link previews, one canonical page, half the build surface. A news-heavy future might revisit it.

Both language strings ship in every page, which inflates HTML slightly — measured against eliminating an entire i18n runtime and keeping every page a server component, it's a cost I'd pay again.

What's next

The structure is already the schema

The contact form is intentionally a frontend stub, and the typed content modules plus JSON feeds are shaped for a headless CMS — when the site needs an editor, the CMS slots into structures that already exist instead of forcing a rewrite.

Building something that has to be taken seriously?

This site had to carry the weight of a national moment in two languages without ever raising its voice. If your project needs that kind of judgment — not just code — let's talk.