Website of C4PO — the Collaboratory for Physical Oceanography at the Virginia Institute of Marine Science, live at oceansensing.org.
Built with Astro. Static output, and close to zero
client-side JavaScript — the exceptions are the theme toggle, the photo
shuffle and lightbox on observation pages, the asset map, and the UTC clock.
The map is the one heavyweight: gliders from four national data centres,
NOAA saildrones, the Argo array, animated currents at two depths,
sea-surface temperature and salinity, isobaths, a detailed coastline and EEZ
boundaries, and ECMWF 10 m wind and 2 m air temperature — with the colour
scale, its range, the isobath opacity and every layer under the reader's
control. The ESPC fields show the step nearest the reader's own clock rather
than a fixed offset from the model run, the currents publishing the two
steps that span a six-hour window so the tide in that model can be stepped
through, and the map's attribution says which hour is drawn and how far from
now it is. The address bar carries the whole view as you move, so the URL is
a link to exactly what is on screen — with a Copy link button beside the
map, since selecting a URL out of a phone's address bar is an awkward
gesture.
It carries two pages. /visualization/
is the general-purpose one and
/observations/hurricanes/
is framed around the storm season. They run the same engine off the same data
and differ only in their preset — which layers open, and where. Adding
another such page means one .astro file passing layers and home to
AssetMap.
It lives in packages/ocean-map, an npm workspace
package rather than a page component, so it can be used elsewhere: see its
README,
EMBEDDING.md for another website, and
PORTING-IOS.md for the native app.
Anything added to it has to keep both of those viable —
BOUNDARIES.md says what that means.
Every push to main deploys automatically via GitHub Actions, and a scheduled
run rebuilds hourly to refresh the map data; the hurricane page picks that up
on its own without losing your place on the map.
All content lives in Markdown and data files — you never need to touch layout code:
| To add… | Edit… |
|---|---|
| a publication | paste its BibTeX entry into src/data/publications.bib |
| a presentation | new entry in src/data/presentations.yaml (type: invited / conference / workshop / outreach) |
| a news post | new .md file in src/content/news/ |
| a person | new .md file in src/content/people/ |
| a research project | new .md file in src/content/projects/ |
| a past project | set status: completed in the project's frontmatter — the Research page moves it to a "Past projects" section automatically |
| a dataset | new entry in src/data/datasets.yaml |
| a software tool | new entry in src/data/software.yaml |
| a significant observation | new .md file in src/content/observations/ — add map: assets for the live asset map, or surveys: entries for dated photo panels |
| a CV item | new entry in the matching src/data/cv/<person>/*.yaml file (grants, advising, service, …) — publications and presentations flow in automatically |
| a member's CV | new directory src/data/cv/<person-id>/ (id matching their file in src/content/people/) with any of the section files — their page appears at /cv/<person-id>/ |
Site title, navigation, contact info, and the list of author names bolded on
the Publications page live in src/config.ts. Colors, fonts, and spacing
live in src/styles/tokens.css.
npm install
npm run dev # dev server at localhost:4321
npm run build # production build into dist/
npm run verify # everything CI checks, in one commandnpm run verify builds, type-checks, checks the docs for drift, and runs the
map, multimap and clock test harnesses — about 900 assertions in all. CI runs the same command and refuses to deploy
if it fails, so running it before you push is the quickest way to find out
whether a change will publish.
The individual pieces, if you want one on its own:
npm run check # type-check
npm run check:docs # docs reference real scripts, real paths, the right URL
npm run test:units # the map's renderer-independent modules, with no DOM at all
npm run test:schema # every published data file against the contract in schema.ts
npm run test:contrast # map colours stay visible, and every conceded clash is named
npm run test:map # asset map, against the built bundle
npm run test:multimap # two maps on one page stay out of each other's way
npm run test:clock # UTC clock, against the built bundleThe test harnesses read from dist/, so build first or they test stale code.
Refreshing map data by hand — CI does all of this on every deploy, so you only need it locally when working on the map:
npm run data # storms, gliders (four regional ERDDAPs), USVs, Argo floats
npm run data:currents # global + regional current grids, surface and 60 m
npm run data:tiles # the 1/12° current tiles (~92 MB per depth, per forecast hour built)
npm run data:fields # global + regional sea-surface temperature and salinity grids
npm run data:wind # ECMWF 10 m wind — the one pipeline with a Python dependency
npm run data:field-tiles # native-resolution Navy field tiles (OISST needs none — its regions already are native)
npm run data:basemaps # re-sample basemap ocean colours (slow; GEBCO's WMS)The tiles are gitignored, so a fresh clone has none and the map simply uses the coarser grids until you build them.
data:wind is the exception to "no Python dependencies": ECMWF packs its open
GRIB2 with CCSDS/AEC, which the standard library cannot decode. Install it
with pip install -r scripts/requirements-wind.txt — binary wheels, no system
packages. Everything else runs on a bare Python 3.
Pushing to main deploys to GitHub Pages, gated on npm run verify — if the
checks fail, nothing is published. The same workflow runs hourly on a schedule
to refresh the map data; that run commits nothing, so the repository does not
grow.
The full-resolution tiles are the exception to "refresh everything hourly": the current tiles are ~92 MB per depth and the Navy temperature and salinity tiles another ~43 MB each, and none of them change until the ocean model runs, once a day at 12Z. CI caches them keyed on that model run and rebuilds only when it advances.
DNS lives at the registrar: apex A records to 185.199.108.153,
185.199.109.153, 185.199.110.153, 185.199.111.153, and a www CNAME to
oceansensing.github.io. The apex is canonical and HTTPS is enforced. The same
zone carries the lab's PrivateEmail MX records — do not let a registrar "Mail
Settings" default overwrite them, or lab email stops.
Working on this repo with Claude Code? CLAUDE.md has the architecture notes
and PLAN.md tracks what is still open.
Copyright (c) 2026 Donglai Gong and C4PO. All rights reserved — see LICENSE. The repository is public so the site can be served from GitHub Pages and so the work is open to inspection; that is not a grant of any licence to reuse it.
Third-party material keeps its own terms: dependencies as declared in
package.json, and the scientific data — served from
ocean-data-repo — under the terms of
the bodies that produced it (GEBCO, Natural Earth, NOAA, IOOS, Ifremer, the
US Navy via HYCOM, Marine Regions and others), each credited on the map.