Geo Nerd is a web-based geography quiz application where players can test their knowledge about world countries, flags, capitals, and map locations. Bilingual (FR/EN), no framework. Live at https://geo-nerd.com.
- Core: Vanilla JavaScript (ES modules), HTML5. No framework.
- Styling: SCSS, compiled to a single
app.cssvia Gulp + Dart Sass + autoprefixer. - Bundling: esbuild (entry
app/js/app.js→dist/app.js,target: es2020, bundled; minified in production builds, unminified in dev, external sourcemap in both). - Build/dev: Gulp 5 + BrowserSync,
gulp-file-includefor HTML partials. - Testing: Vitest unit tests in
tests/(npm test). - Animation: GSAP + Draggable (vendored in
dist/libs/, loaded via<script>). - Audio: Web Audio API synthesized tones (no audio files).
- Deployment: custom SFTP script (
deploy.js) usingssh2-sftp-client. - Server: clean/extensionless URLs (
/page-name→/page-name.html).
Flag Nerd and Capital Nerd come in Classic, Hard and Speed Run; Map Nerd in Classic and Hard; Country
Nerd (name a country per letter A–Z) in Classic and Hard; plus a deterministic Daily Challenge
(Classic/Hard, 20 questions, same for everyone each day). See app/js/games/.
/app: source files (edit here)./app/jsapp.js: entry point; loads data, setswindow.geoNerdApp, instantiates per-page modules./games: one file per game mode (BaseGame.js,BaseQuizGame.js,BaseSpeedRun.js+ implementations)./utils:LanguageManager.js(i18n),StringUtils,ArrayUtils,ScoreUtils,MapInteraction,SoundManager,FeedbackEffects./services:GeoNerdNavigation.js(routing),StorageService.js,MobileMenu.js./pages: page controllers (HomeCards,Scores,Training).
/app/pages: HTML templates +partials/(usesgulp-file-include). Containsworld-countries.svg./app/scss: styling — top-level partials (_variables,_fonts,_layout,_titles) +pages/+partials/; entryapp.scss./app/data: country JSON./app/img,/app/fonts,/app/libs,/app/favicon: static assets.
/dist: fully generated build output — do not edit.app.js/app.css(esbuild/sass), HTML (fileInclude), and all assets copied fromapp/(theassetstask).
Note: UI translations live in
app/data/i18n/{fr,en}.json(imported and bundled by esbuild, not served). Thecountries-*.jsonfiles inapp/data/hold country data only.
SPA-like behavior. GeoNerdNavigation.js handles clean URLs; the server maps /page-name to
/page-name.html in dist.
LanguageManager.js is the i18n core. UI strings live in app/data/i18n/{fr,en}.json; they are
import-ed and inlined into the bundle by esbuild at build time, so t(key) resolves synchronously
(no runtime fetch). Adding a language = add a JSON file with the same key tree + import it.
- Language is URL-derived: paths under
/en/...are English, everything else French. The FR and EN builds are produced separately —dist/*.htmlanddist/en/*.html— viagulp-file-includewith alangcontext variable. - HTML hooks:
data-i18n(textContent),data-i18n-html(innerHTML),data-i18n-placeholder(placeholder attr);translatePage()runs on load.LanguageManageralso applies French typography (non-breaking spaces before!?:;). - SEO meta: per-page
<title>/description/OG/Twitter/canonical are baked into the static HTML at build by the gulpinjectMetatask (it resolves the__META_TITLE__/__META_DESCRIPTION__tokens in_header.htmlfrompages.<slugCamelCase>in the i18n JSON, so crawlers and social unfurlers that don't run JS get them), then re-confirmed client-side byupdateMetaTags()(which also handles language switches). Source of truth stays the i18n JSON. - Country data is language-specific:
data/countries-{lang}.json(full set) anddata/countries-easy-{lang}.json(reduced set used by Map games). - localStorage keys are language-prefixed (
{lang}.{key}) by default, so in-flight game state is per language; shared keys (those ending in.best,.gamesPlayed,.cumulativeScore, plusgeonerd.muted) are stored unprefixed so best scores and cumulative stats persist across FR↔EN.
- Inline SVG (
app/pages/world-countries.svg); playable countries carry the.onuclass, others are dimmed. MapNerdClassic.js(point-and-click) vsMapNerdHard.js(point-then-type).- Zoom/pan via GSAP (
MapInteraction.js).
npm run deploy builds and pushes /dist to the server via SFTP. Connection details live in the
git-ignored sftp-config.json (see sftp-config-sample.json).
npm install: install dependencies.npx gulp: build + watch + live-reload. BrowserSync proxies thegeonerd.localvhost, so you need a local web server servingdist/mapped togeonerd.localin/etc/hosts.npx gulp build: production build intodist/(no watch/server).npm run deploy: build then pushdist/to production via SFTP.npm test: run the Vitest unit tests (intests/).- Versioning:
npm run release -- <major|minor|patch|X.Y.Z>(runsrelease.js) bumpspackage.json, inserts a datedCHANGELOG.mdskeleton, commitsRelease X.Y.Zand creates the matching tag. The version is injected into the HTML (@@version) as a?v=cache-buster onapp.js/app.css, so bump it on every deploy.