A proof-of-concept OpenGeoMetadata community frontend for configurable, static-site-friendly geospatial discovery using metadata served by the OpenGeoMetadata API.
This project is evolving the BTAA Geoportal frontend into a reusable OpenGeoMetadata viewer that can be branded for different institutions without forking the application. The goal is a lightweight React/Vite app that runs well on static hosting, keeps institutional content in configuration, and preserves BTAA as the reference preset.
- Searches and displays geospatial records from the OpenGeoMetadata API.
- Provides search, map, bookmark, and resource detail views.
- Uses
theme.yamland optionalthemes/*.yamlfiles for institution branding, navigation, homepage content, API paths, locale settings, and deployment-facing metadata. - Supports localized shared UI strings through
src/i18n/. - Builds as static assets that can be hosted on services such as GitHub Pages.
The active application lives in src/:
src/config/institution.tsparses the defaulttheme.yaml, merges optional theme variations fromthemes/, and applies theme settings.src/services/api.tstalks directly to the OpenGeoMetadata API.src/i18n/contains shared message catalogs and locale helpers.src/components/andsrc/pages/implement the viewer experience.public/contains static assets used by themes and the generated site.scripts/contains scaffolding, site generation, and QA helpers.
The app/ and server/ directories are legacy or transitional code from an
earlier SSR/BFF setup. Prefer the SPA path in src/ for new product work unless
a task explicitly calls for those older surfaces.
Install dependencies:
npm installCreate a local environment file:
cp .env.example .envStart the development server:
npm run devBuild the static site:
npm run buildPreview the production build:
npm run previewFor the full adoption workflow, including GitHub Pages setup, theme modeling, and component customization guidance, see docs/adopting-github-pages.md.
The main configuration file is theme.yaml. It should read like a complete
starter site for one institution, not a packed list of every example.
The default theme is opengeometadata, a fictitious OpenGeoMetadata institution
with a Bauhaus-inspired starter palette. Additional examples live as one theme
per file in themes/, including themes/btaa.yaml as the reference preset for
BTAA Geoportal behavior and localization.
Theme configuration controls:
- site title, description, locale support, and web app manifest colors
- optional localized sitewide notice banners
- theme-specific favicon, Apple touch icon, and PWA install icons
- institution name, logo, header lockup, and hero copy
- brand colors, fonts, and optional institution-hosted font stylesheets
- navigation links, utility links, and calls to action
- homepage hero content, featured records, collection spotlights, media, and blog modules
- footer layout, links, institutional address, and copyright text
- backend API root endpoint, endpoint paths, optional public API key, and default query parameters
The default backend API root is https://ogm.geo4lib.app/api/v1/. Override
api.base_url in theme.yaml when an institution needs to point at a different
compatible API deployment.
Set navigation.cta_style: utility when a theme should keep the CTA in the
right-side header slot but render it with the same compact treatment as utility
links. Omit the field, or set it to button, for the default button treatment.
Set homepage.hero_map.center as [latitude, longitude] and
homepage.hero_map.zoom to choose the homepage map's initial camera. Use
homepage.hero_map.initial_pan_px: [0, 0] when a theme should not apply the
default horizontal map pan after initial render.
Set site.banner when a theme needs a sitewide message above the header. The
banner text supports localized values. Use tone: neon only for temporary,
high-visibility notices such as experimental proof-of-concept deployments.
site:
banner:
enabled: true
tone: neon
text:
en: 'NOTICE: This is an experimental proof-of-concept.'Static browser deployments can also set api.public_api_key to a rate-limited,
browser-safe key. The viewer sends it as Authorization: Bearer <key>. Treat
this value as public because it is embedded in the built site and shares one
quota across all visitors.
Use only capped, browser-safe API keys in this frontend. A key in a static build is visible to anyone who opens the site, so it must not be a private backend or administrator secret.
For the repository's GitHub Pages deployment, add the key to the Pages workflow environment so it applies to the built site:
env:
VITE_API_PUBLIC_KEY: browser-safe-rate-limited-keyFor an institution-specific theme, add the key beside the API root:
api:
base_url: https://ogm.geo4lib.app/api/v1/
public_api_key: browser-safe-rate-limited-keyBoth approaches send the key on API requests as Authorization: Bearer <key>.
Prefer VITE_API_PUBLIC_KEY when one GitHub Pages build should use the same key
for every included theme variation. Prefer api.public_api_key when a copied or
forked theme needs to carry its own public API credentials.
For a new institutional deployment, copy theme.yaml for a single-site build or
add a new themes/<theme-id>.yaml file when you want it available alongside the
included examples. Prefer theme fields over hardcoding institution-specific
behavior in components.
Each theme can define its browser and install icons through site.icons.
Included themes keep their packs in public/theme-icons/<theme-id>/:
site:
icons:
manifest: /theme-icons/my-institution/manifest.webmanifest
favicon: /theme-icons/my-institution/favicon.ico
favicon_svg: /theme-icons/my-institution/favicon.svg # optional
apple_touch_icon: /theme-icons/my-institution/apple-touch-icon-180x180.png
pwa:
- src: /theme-icons/my-institution/pwa-64x64.png
sizes: 64x64
type: image/png
- src: /theme-icons/my-institution/pwa-192x192.png
sizes: 192x192
type: image/png
- src: /theme-icons/my-institution/pwa-512x512.png
sizes: 512x512
type: image/pngnpm run generate:site writes a web app manifest for every configured theme.
Theme manifests use path-based hash URLs such as /my-institution/#/ so a
launched PWA opens with the installed theme active. When adding a theme, create
the matching icon pack before publishing the build.
Most deployment behavior should come from theme.yaml. Environment variables
are reserved for local development, legacy fallbacks, or deployment secrets.
Common variables:
VITE_BASE_URL: base path for Vite builds, usually/.VITE_API_BASE_URL: legacy API base URL fallback when a theme does not defineapi.base_url. Preferapi.base_urlintheme.yaml; the default public root ishttps://ogm.geo4lib.app/api/v1/.VITE_API_PUBLIC_KEY: optional browser-visible fallback key sent asAuthorization: Bearer. This is useful for deployment-wide static builds, such as GitHub Pages workflows.VITE_CSRF_TOKEN: optional CSRF token placeholder for deployments that need it.VITE_APP_VERSION: version label sent in API diagnostic headers.VITE_ENABLE_DEBUG_LOGS: enables client debug logging when true.VITE_TURNSTILE_ENABLED: enables or disables Turnstile checks.VITE_TURNSTILE_SITE_KEY: Cloudflare Turnstile site key.VITE_TURNSTILE_ACTION: Turnstile action name.
Do not commit real .env files or private API keys. A capped public browser key
may live in theme config only when it is intentionally safe to expose.
npm run dev # generate site files and start Vite
npm run build # generate site files and build static assets
npm run preview # preview the built site
npm run test # run Vitest
npm run test:ci # run Vitest once with coverage thresholds
npm run lint # run ESLint
npm run typecheck # check application, tests, and Vite configuration
npm run lint:fix # run ESLint with automatic fixes
npm run format # run Prettier
npm run format:check # check formatting
npm run scaffold # create a starter themes/<theme-id>.yaml file
npm run deploy # publish dist/ with gh-pagesUse the scaffold helper to create a starter theme file:
npm run scaffold -- my-institution "My Institution"The scaffold is written to themes/my-institution.yaml. The root theme.yaml
is still the simplest copyable example when building a single institution site.
When adding a new theme:
- keep one institution per YAML file
- keep
btaaworking as the reference implementation - keep copy localizable by using localized objects for configurable text
- add a favicon, Apple touch icon, and PWA icon pack under
public/theme-icons/<theme-id>/ - use theme fields for institution names, logos, links, colors, and homepage content
- avoid server-only assumptions so the site remains static-host friendly
- update this README when new public configuration fields are introduced
This app builds to dist/ and can be hosted as static files.
npm run generate:site writes a deny-all public/robots.txt so themable
preview and pilot sites do not invite crawler traffic before an institution is
ready to publish.
npm run build also emits dist/404.html and dist/500.html as copies of the
built SPA entry point, plus dist/<theme-id>/index.html for every configured
theme. GitHub Pages deployments use hash routing for all themes, with URLs such
as /ogm-discovery/unr/#/resources/<id>. Hosts that support a custom
server-error fallback can serve 500.html.
The repository includes a GitHub Pages workflow in
.github/workflows/deploy.yml. Review its branch trigger and required secrets
before relying on it for a new deployment.
Manual deployment through the gh-pages package is available with:
npm run deploytheme.yamlis the source of truth for the default institution branding and deployment configuration;themes/*.yamlfiles provide optional variations.- Shared UI copy belongs in
src/i18n/messages.ts. - Institution-specific copy should stay in localized theme fields.
- Generic product work should usually happen in
src/, notapp/orserver/. - Keep the viewer map-forward, accessible, localizable, and static-host compatible.
Resource pages use OGM Viewer
(ogm-viewer 1.5) for map and IIIF previews, layer controls, and feature inspection.
The viewer loads on demand and receives the Aardvark record already fetched by the
app, preserving direct service URLs and static hosting support. Object and JSON
string reference fields are normalized; the API's selected preview and geometry
provide fallbacks when references or bounds are missing. COG URLs retain the
resource version parameter used for cache invalidation.
OGM Viewer owns its preview tabs and controls. The surrounding loading/error messages are localized; upstream viewer controls currently use English. oEmbed content is displayed in a sandboxed iframe because OGM Viewer does not provide an oEmbed preview. Featured homepage layers and discovery maps keep their existing implementation; GeoBlacklight remains a dependency for the homepage layers.
For deployments with a Content Security Policy, follow OGM Viewer's
CSP requirements,
including worker-src blob:, script-src 'wasm-unsafe-eval', and connect-src data:
alongside the metadata, basemap, and layer service origins.
Search-result and related-item thumbnails use the canonical API thumbnail
resolver, with the BTAA Geoportal's variant=icon-gradient fallback for list
cards. Missing, restricted, or failed thumbnails show a resource-class icon;
HTTP-success SVGs containing "Thumbnail unavailable" are never displayed.
Ready immutable thumbnails retain normal browser image loading. Queued captures
retry at most 12 times and stop when the card is removed. For cross-origin
pending-thumbnail updates, the API must expose X-Thumbnail-Pending and
X-Placeholder through CORS; unavailable SVG detection also works without those
headers. Requests use the configured API base URL and need no frontend proxy.
Discovery uses MapLibre GL JS for its hero, search filter, search results,
bookmarks, and resource location maps. H3 aggregation still comes directly from
api.map_h3_path; cell IDs, search filters, counts, and resolution levels 2–8
retain the existing API contract. The accessible hex table remains available.
OGM Viewer continues to own resource previews and IIIF rendering.
The Place facet fits each new search to its full H3 result extent using a resolution-2 request without a viewport bounding box. Active search filters still apply, and explicit location bounds take precedence. Panning, zooming, and pagination preserve the camera; visible hexes continue loading for the current viewport. Empty results leave the camera in place.
map in theme.yaml (or an institutional theme) configures the basemaps:
basemaps: ordered entries with a stableidand localizedlabel. Supplystyle_urlfor a MapLibre style, ortiles,tile_size(default 256),max_zoom(default 19), andattributionfor raster tiles. Optionallogocontainssrc,href, and localizedalt.default_basemapandfallback_basemap: entry IDs. Both default to OpenFreeMap Liberty. Shipped choices are Liberty, Positron, and Dark. Unknown or retired saved choices (including Maptoolkit and Esri) use the configured default.viewer_light_styleandviewer_dark_style: style URLs passed through OGM Viewer's public properties; defaults use OpenFreeMap to avoid CARTO.
A style-load error, a 15-second style timeout, or three distinct basemap tile failures within ten seconds switches Discovery maps to the fallback. Each provider is attempted once; if both fail, resource overlays remain on a neutral background with a retry button. Overlay/API failures do not change providers. Automatic fallback does not overwrite the visitor's saved basemap choice.
OpenFreeMap supplies all shipped basemaps, viewer styles, and native-generated campus/resource map images. Preserve the style's OpenFreeMap, OpenMapTiles, and OpenStreetMap attribution. See the OpenFreeMap setup guide. Hosted service availability is not guaranteed. Institutions can configure their own style endpoints. With the default Liberty selection, a basemap failure goes directly to the neutral background and retry control; switching between OFM styles does not provide independent-provider resilience. No persistent tile cache is added.
Existing theme hero centers and featured camera centers stay [latitude, longitude]; existing camera zoom values retain their Leaflet-era scale for
backward compatibility. They are converted at the boundary to MapLibre
[longitude, latitude] and zoom minus one. New internal map geometry uses
longitude first. Browser map requests remain direct HTTPS requests; CSP must
permit configured styles, tiles, sprites, glyphs, logo origins, and MapLibre's
worker (worker-src 'self' blob:). The SPA entrypoint explicitly bundles the
MapLibre module worker through Vite so development and subpath deployments
resolve it correctly. Keep that initialization before rendering maps. No server
proxy is required.
Backend-generated static-map images, external embeds, and the legacy app/
SSR implementation are separate from this frontend migration. Legacy Leaflet
assets/dependencies needed by app/ remain isolated from the SPA entrypoint.
Partner campus backgrounds use API-generated MapLibre PNGs behind the bundled
school logos. Discovery requests /static-maps/institutions/{slug}/native with
the configured campus center and zoom. Deploy the API's optional native-map
renderer/Celery integration before enabling these images in production; older
APIs return 404 and Discovery retains its branded fallback. It never requests
legacy CARTO campus images. Cold renders retry briefly without hiding the logo.
Images normally use the active API base. For local integration testing only, set
VITE_STATIC_MAP_API_BASE_URL=http://127.0.0.1:3007/api/v1 when building to send
campus and resource geography images to the local asset API while search still uses the configured API.
Omit this override from production builds. Campus-image provider credits are
visible below the partner grid; this integration currently uses OpenFreeMap.
Search-result geography thumbnails request /static-maps/{id}/geometry/native.
This endpoint generates missing images through Celery and stores them in the
existing content-addressed asset system. Discovery bypasses legacy API static-map
URLs (including immutable hashes containing CARTO error tiles); custom external
map URLs remain supported. Deploy the native API route and worker before this
frontend change. Pending images retry briefly, then show a localized unavailable
state rather than falling back to legacy tiles. Existing immutable assets are
not overwritten.
Static-map loading uses status-aware fetches near the viewport (400px preload
margin). Only pending/rate-limited responses (503/429) retry, using Retry-After;
missing records fail immediately. Successful URLs stay stable so browser caching
works. Native endpoints require CORS access from the frontend origin; CSP must
allow the asset API in connect-src and blob: in img-src. Custom external
map images continue to load directly. Search cards request the 384×288
?size=thumbnail variant; full-size assets remain available without this query.
The API caches successful native responses in the browser for one hour.
For predictable first-page speed, prime campus maps and frequently viewed resource thumbnails before deployment. A cold render still depends on remote basemap data and available worker capacity. The local preview harness now loads missing public resource geometry into its isolated test database on demand; this behavior is not installed in the production API.