Bilingual site and CMS for an education association
An association connecting Chinese and Bangladeshi universities needed its whole site in English and Chinese, edited by staff with no developer. I built it end to end, then ran a full security hardening pass on the live system.


How authorship was measured. 31,666 of 35,711 lines, measured with per-file git blame across all tracked text files, excluding lockfiles and binaries.
The problem
A non-profit association connecting Chinese and Bangladeshi universities needed its entire website published in English and Chinese, edited by staff with no developer involved: programmes, services, events, news, leadership, membership tiers. It also had to stand up to the attention a public association site attracts, from bots as much as from people.
What I built
Bilingual content is stored as pairs. Every item exists as an English row and a Chinese row sharing a slug, written together in one operation. The update path refuses to run if the pair count is not exactly two. Bilingual sites almost always rot into English-only rows with orphaned translations; this makes that structurally hard. A script walks both locale catalogues and confirms that the 174 keys are identical in both directions.
Every write route is authenticated and authorised. Eighteen write handlers across the API, each checked against the session and the caller’s role, verified route by route. Members cannot approve their own applications, drafts stay private until they are published, and a failed file read never reveals the server’s layout.
Rate limiting is two layers. A signed state cookie with four rule tiers, and behind it a separate in-process counter that is checked first, because a cookie-only limiter can be reset by discarding the cookie.
The upload path rejects on content length before it buffers anything, then re-checks the bytes actually read, because a client-supplied length cannot be trusted. Everything accepted is transcoded to WebP under a random filename, which is what closes the stored-XSS vector rather than filtering extensions.
Image loading is locked to known hosts, so the site cannot be used as an open image proxy.
The hardening pass
Once the site was live I ran a full audit of it and kept a severity-graded register: 68 findings across security, accessibility, SEO, functional bugs and the design system, each with a file and line reference. Every security finding is closed in code, with the commits recorded against the register. The register itself is a document a client can read.
- Next.js 16
- TypeScript
- MongoDB
- Auth.js
- sharp
- nginx
- PM2