Static, country-aware portfolio. Five regional versions built from one template and one stylesheet, plus server-side geo routing at the Vercel edge.
middleware.js edge geo routing (server-side, runs before any file is served)
index.html root: region picker + local/static fallback router
templates/page.html THE template. Every regional page is rendered from this file.
scripts/build.mjs the build: data -> five pages + sitemap.xml + robots.txt
assets/
data.base.json everything shared between regions
data.uk|in|us|jp|de.json per-region overrides only (see assets/DATA-CONTRACT.md)
css/site.css THE stylesheet, shared by all six pages
js/site.js THE script, shared by all six pages
images/, favicon.*
cv/ per-country CV PDFs (see assets/cv/README.md)
uk/ in/ us/ jp/ de/ GENERATED, do not edit index.html inside these
npm run build # regenerate uk/ in/ us/ jp/ de/ index.html, sitemap.xml, robots.txt
npm run check # render everything to memory and fail on any data problem
npm start # serve locally on http://localhost:3000vercel.json runs npm run build on every deploy, so a data-only change is deployed
correctly even if someone forgets to run the build locally. Never hand-edit
uk/index.html and friends, the next build overwrites them. Edit the data or the
template.
middleware.js is Vercel Edge Middleware. It runs on the server before any static file
is served, which is the only mechanism in a static site that can actually enforce this.
It reads the x-vercel-ip-country header Vercel injects at the edge:
| Country header | Region |
|---|---|
IN |
/in/ |
GB |
/uk/ |
US |
/us/ |
JP |
/jp/ |
DE |
/de/ |
| anything else | /uk/ |
The /uk/ default is deliberate, not an accident. Visitors from the 190-odd
countries with no page of their own get the UK version because it is the most
conservative of the five: British spelling, no photo, no personal data beyond contact
details. It is the safest thing to show a stranger from anywhere. Change
DEFAULT_REGION in middleware.js if that judgement changes; it is a single constant.
A request for the wrong region is redirected (HTTP 307), not merely discouraged.
An Indian visitor who types /uk/ is sent to /in/.
Append ?region=uk to any URL. The middleware sets a pn_region cookie (30 days,
SameSite=Lax) and redirects to the clean /uk/ URL, bypassing geo entirely from then
on. This is how Prajwall sends a UK recruiter the UK page while sitting in Bengaluru:
https://sprajwallnarayana.com/uk/?region=uk
The recipient gets the same cookie, so the link works for them too, from any country.
Every page carries a visible bar at the top: "You are viewing the United Kingdom
version of this site" with a region <select>. Choosing a region writes the same
pn_region cookie and navigates. Being wrong about someone's country is never a dead
end, and it degrades to plain links inside <noscript>.
Precedence: ?region= → pn_region cookie → geo-IP header → default.
Edge geo-IP is accurate in the common case and useless against anyone who does not want it to work:
- A VPN, a corporate proxy or a mobile carrier routing through another country presents a different country code. The visitor sees that country's page. No public website can prevent this.
- A cookie is client-side state. Anyone can set
pn_regionby hand. - Crawlers are exempt on purpose (below), so a spoofed
User-Agent: Googlebotreaches any region.
This raises the floor, a casual visitor lands on the right page and cannot browse the others by guessing URLs. It is not a security boundary and nothing behind it should be treated as private. Everything in these pages is public information.
Googlebot crawls almost entirely from US IP addresses. With the redirect applied to it,
Googlebot would be pushed to /us/ on every request and would never see any other page.
So ALLOW_CRAWLERS in middleware.js exempts known crawler user-agents from the geo
redirect. That is the one deliberate hole in the routing; the cost is stated above.
On top of that:
- Every regional page is self-canonical (
/uk/canonicals to/uk/, etc.). - Only the default region (
/uk/) is indexable. The other four carry<meta name="robots" content="noindex,follow">, andsitemap.xmllists only/and/uk/. robots.txtallows crawling everywhere on purpose. A crawler has to be able to fetch a page to read itsnoindex; aDisallowwould hide the tag and leave the pages eligible for indexing from external links.
The tradeoff, stated rather than guessed at: the five pages are ~90% identical
content, so letting all five compete splits ranking signals across near-duplicates, and
a searcher who clicked a /jp/ result from Germany would immediately be redirected, which looks broken and wastes the click. One indexable page avoids both. The cost is
that regional pages earn no organic traffic of their own; they are for links Prajwall
sends and for visitors the edge routes. The alternative, indexing all five with
hreflang, is the right pattern for language variants served without forced
redirects, and it fights the geo redirect rather than complementing it. If the site ever
drops the redirect, switch to hreflang and delete the noindex tags; that is a
five-line change in scripts/build.mjs.
To make a different region indexable, set "flags": { "noindex": false } in that
region's data file. null (the default) means "auto": indexable only if it is
site.defaultRegion.
The site is plain static files with no runtime dependency on the edge. Opened from
file:// or served by any static host:
index.htmlroutes using a stored preference (pn_regioncookie, thenlocalStorage), and shows the region picker when there is none.- Every regional page still works standalone, all content is rendered into the HTML at
build time, so nothing depends on
fetch, and the pages are readable with JavaScript disabled. - The switcher and
?region=still work; they persist tolocalStorageas well as the cookie. Only the enforcement is gone, which is expected: there is no server.
- The job title is AI Engineer everywhere, matching the offer letter. Nothing on this site may say otherwise, a title that disagrees with the offer letter breaks under background checks and visa sponsorship.
- The pages must stay balanced between AI/product work and platform/DevOps work. Country tuning changes conventions, spelling, length and visa framing. It must not tilt him towards either side.
- No LinkedIn URL anywhere. The account is restricted; the omission is deliberate. Use GitHub and this site.
- Nothing goes on these pages that is not a verified fact. No invented metrics, dates, employers or technologies.
- No CV is committed right now. The old
assets/resume.pdfwas removed rather than reused: it titled him "Software Development Engineer" and "DevOps Team Lead", and it printed a LinkedIn URL. Both break the rules above, and a linked PDF is part of the page. Until a per-country CV lands inassets/cv/, the build omits the CV button entirely, seeassets/cv/README.md. The old file is still in git history.
- One 100 KB inline-CSS page became six pages of ~27 KB each sharing one cached stylesheet and one cached script.
- The 60 KB OS-desktop script and 72 KB terminal script that the old page loaded but
never used are gone, along with the dead
index2.htmlthey belonged to. favicon.icowas a 48 KB JPEG with an.icoextension. It is now a 5 KB 32/16px icon plus a 310-byte SVG.logo.jpgandS_Prajwall_Narayana.jpgwere byte-identical 48 KB copies of the same file. One 35 KB 300×300 image remains.- The CV PDF is not fetched on first paint. The CV modal sets the iframe
srconly when it is opened, and never on mobile, and the button is not rendered at all unless the file a region points at actually exists. - The committed
.DS_Storeis gone and stays gone via.gitignore.