Every plot has a shape, a road, and a direction it faces.
A bilingual property platform for a Kathmandu agency that verifies land titles against government records before it lists anything. Land, houses and apartments for sale or rent — searched in ropani and aana, dated in Bikram Sambat, and served fast to a buyer arriving from one Facebook link on a mid-range phone.
Nepali land doesn't fit a template built for square feet and Gregorian dates.
Off-the-shelf listing platforms assume one unit system, one calendar and one language. Kathmandu buyers ask how many aana, how wide the road is and which way the plot faces. Terai buyers ask in kattha. Non-resident Nepalis ask in English on a Viber call.
The agency's edge was verification — title checks and physical site visits — but its listings lived in Facebook posts. It needed a site that carried that trust, ranked for every district it worked in, and could be run by two or three staff without a developer.
Solo build,
design handoff in
Seven prototype screens and a design system came in; the data model, both locales, the admin, the deployment and every guard script came out — in under two weeks.
Next.js 15 · React 19 · Tailwind 4 · shadcn · next-intl
PostgreSQL · Prisma 7 · 17 models · tsvector search
MapLibre GL + OSM tiles · sharp · local-disk storage
Vitest · 40 test files · Lighthouse + JS-budget harness
Next standalone · Node on Nepal-hosted Webuzo · Resend
A storefront for land,
and a desk to run it
Filter by type, price, area, location, road width and facing direction — every filter lives in the URL and round-trips.
Land, house and apartment each get their own spec layout, gallery, map, deep links to WhatsApp, Viber and phone, and a print sheet.
Ropani-aana-paisa-daam and bigha-kattha-dhur conversion, lakh/crore digit grouping, a Bikram Sambat calendar module.
District and municipality landing pages that exist only while they have an active listing, plus sitemap, canonicals and JSON-LD.
Cookie-keyed shortlist, saved searches and view tracking, hydrated client-side so pages stay static.
English and Nepali across the public site and the admin, with a test that fails if the two message files drift.
Ropani, aana, paisa, daam
Nepal measures hill land in ropani and Terai land in bigha, prices it in lakh and crore, and dates it in a calendar roughly 57 years ahead of the one your server runs on. None of that can be an afterthought in a property database.
- Land stored canonically in square metres; built-up area separately in square feet.
- Hill units (ropani) and Terai units (bigha) derived for display, each with its own slider ceiling.
- Prices derived per type — a house price is never divided by its plot area.
- Dates stored in UTC, displayed in Bikram Sambat at UTC+05:45.
Fast on 3G,
honest to Google
Most visitors open one listing from a shared link on a phone and never see the homepage. That page has to be prerendered, correctly indexed and light. Three separate bugs proved that the framework's defaults would quietly break each of those, so each now has a guard in the build.
The baseline audit measured LCP between 3.4 and 4.6 seconds on throttled mobile before the fixes. The commit history keeps the corrections too — a claimed cause that turned out untested was deleted rather than left standing.
Bugs the green
checks didn't see
A loading boundary on the properties segment streamed a 200 header before notFound() ran, so drafts and unknown slugs returned 200 to search engines. Moved search into its own route group and added a status-code check against the production build.
Why — A listing site that indexes its own drafts is lying to Google. Only a real HTTP check catches this class of bug.
A single cookie read in the render tree quietly demoted 19 of 53 location pages to dynamic rendering. Every check was green. Visitor state was pulled out to the client and a prerender-manifest assertion now guards the build.
Why — Static is a property you have to defend, not a setting you turn on.
Storing area in the unit the seller typed would have meant converting in every query and every filter. One canonical column, with hill and Terai unit families derived at the edge, keeps search honest across regions.
Why — A Kathmandu buyer thinks in aana; a Jhapa buyer thinks in kattha. The database shouldn't care.
A district page and a listing live at the same URL depth so that a Facebook link to either reads clean. A cached resolver tries locations first, then listings, and a registry rejects any slug that would collide.
Why — Most visitors arrive from a single shared link on a phone. The URL is the product's first impression.
Tiles rendered but markers never appeared: the worker path 404'd. A postinstall script copies the worker into public and the map component points at it explicitly.
Why — A property map with no pins is worse than no map. The fix had to run on every install, not live in a README.