Static, zero-build landing pages for ONDC metro station QR codes. Each physical QR at a station points to a city page that lists the authorised ONDC buyer apps a commuter can use to buy a metro ticket.
Live at metro.ondc.tech.
The site has two levels, each a thin static HTML shell rendered by one shared script. Every real URL is root-relative:
/ → pick a city
/<city>/ → pick a buyer app to book with
For example:
/bangalore/
/delhi/
Generated pages live at the repo root (not under public/) so that the
bare / URL itself resolves — public/ holds only the shared,
non-generated assets (public/js/, public/css/, public/images/).
There is no build step for the runtime — every page is a plain .html file
that:
- Sets its own
<title>/<meta>tags (so link previews and SEO work without JS). - Declares which city it needs via a
data-groupattribute on<body>. - Links shared stylesheets (
public/css/picker.cssfor root city picker andpublic/css/entity.cssfor city buyer pages) instead of inlining CSS per page. - Loads
public/js/app.js, which fetchesdata/entities.jsonand renders everything dynamic — the navbar, footer, header logo, and the list itself (cities or buyer apps) — into placeholder elements (#navbar-slot,#footer-slot,#header-logo,#main-content).
All of those HTML files are generated — see Generating pages
below. You never hand-write or copy city folders; you edit data/entities.json
and run one script.
index.html # city picker (generated)
bangalore/index.html # buyer-app list (generated, data-group="bangalore")
delhi/index.html
…
data/entities.json # single source of truth for all content + site config
data/analytics.json # per-city GA4 measurement IDs (optional per city)
scripts/generate.mjs # reads JSON, writes index.html + <city>/index.html
scripts/templates/root.html # template for the city picker
scripts/templates/group.html # template for the buyer-app list
.github/workflows/deploy.yml # generate + deploy to GitHub Pages on push
public/js/app.js # shared renderer + navbar/footer + GA4 tracking
public/css/picker.css # shared styles for root city picker
public/css/entity.css # shared styles for city buyer pages
public/images/buyers/ # buyer app logos + metro logos
public/images/ondc-logo.svg
public/images/favicon.png
CNAME # custom domain for GitHub Pages
All city, buyer-app, and site-level data lives in data/entities.json —
nothing else needs to change for day-to-day updates.
{
"site": {
"productName": "Metro Ticketing",
"orgName": "ONDC"
},
"groups": [
{
"slug": "bangalore",
"name": "Bangalore",
"productName": "Metro Ticketing",
"title": "Get Bangalore Metro Digital Tickets via ONDC",
"subtitle": "Book your tickets from the below apps",
"logo": "/public/images/buyers/banglore_metro.svg.webp",
"buyers": [
{
"label": "EaseMyTrip",
"logo": "/public/images/buyers/EaseMyTrip%20Logo.svg",
"url": "https://www.easemytrip.com/metro/"
}
]
}
]
}site: project-wide defaults (productName,orgName) for titles/meta.groups: cities. Each group has abuyersarray (no nested entity level).productName(group): optional override for the navbar label (Metro TicketingvsDiscover Metro).logo(buyer): optional. If omitted, the row falls back to a colored dot.darkLogo: optional; applies a dark background behind the buyer logo (e.g. Namma Yatri).androidUrl/iosUrl: optional platform-specific deep links (e.g. Kochi1).- Asset filenames with spaces must be percent-encoded in the JSON.
Per-city GA4 IDs live in data/analytics.json. Only cities listed there
load gtag. Cities without an entry have no analytics (same as the previous
Delhi / Mumbai / Hyderabad pages).
Add an entry to that city's buyers array and re-run the generator.
Add an entry to the groups array in data/entities.json (and an analytics
entry in data/analytics.json only if that city should track GA). Re-run the
generator.
node scripts/generate.mjs
This reads data/entities.json + data/analytics.json, validates them
(unique slugs, https buyer URLs, referenced local images exist, etc.), and
writes index.html plus every <city>/index.html at the repo root. It also
deletes any previously generated city folder that's no longer in the JSON.
Generated HTML is git-ignored — rebuild before every deploy.
node scripts/generate.mjs
python3 -m http.server 8000
Then open http://localhost:8000/ or http://localhost:8000/bangalore/.
Deployed to GitHub Pages via .github/workflows/deploy.yml:
on every push to main, it runs node scripts/generate.mjs and publishes the
result. Generated HTML is never committed — it's rebuilt fresh on every
deploy, so it can't drift out of sync with data/entities.json.
This requires the repo's Pages source (Settings → Pages → Build and deployment) to be set to "GitHub Actions" (a one-time setting).
CNAME— custom domain (metro.ondc.tech), included in the deploy artifact.
Do not rename events or parameters — existing GA4 reports depend on them.
Cities with a measurementId in data/analytics.json load GA4 via the
generated page's gtag.js script tag. public/js/app.js then:
| Call | When | Parameters |
|---|---|---|
user_properties.platform_os |
On load | Android / iOS / Other |
platform_detected |
DOMContentLoaded on GA-enabled city pages |
platform_os |
buyer_app_click |
Tap on a buyer app link | app_name, platform_os, destination_url |
If a buyer has androidUrl / iosUrl, the click handler may preventDefault
and window.open the platform-specific URL (same as the previous Kochi1
behaviour). The root city picker does not load GA.
Current measurement IDs:
| City | Measurement ID |
|---|---|
| Bangalore | G-17W29WN58K |
| Kochi | G-17W29WN58K |
| Chennai | G-5F86E5ZX67 |
| Delhi, Mumbai, Hyderabad | (none) |
ONDC (Open Network for Digital Commerce) is an initiative by the Government of India to democratise digital commerce. This site is part of ONDC's metro ticketing discovery effort.