Publish static output
An operator produces the files before anyone views a page. A website can host those files directly or render them during its own build.
Responsibility sequence
- The forecast engine generates model-dependent values. Quantities that require inputs beyond the published document stay in the engine.
- Consumers validate at the document boundary. They accept only profile documents that satisfy the exported contract.
- The operator publishes static artifacts, putting versioned JSON and generated assets on infrastructure the operator controls.
- Renderers work from the artifact. They build a scene and serialize SVG in Node, a worker, or another non-DOM runtime.
Know the output tree
The CLI writes one directory per selected model beneath --output:
public/data/ <model-slug>/ manifest.json sites/ <site-slug>.json history/ <site-slug>/ <YYYY-MM>.jsonl.gz <YYYY-MM>.index.jsonEach month archive has a <YYYY-MM>.index.json next to it. This is its
advisory byte-offset sidecar index,
rewritten from the archive bytes on every append. Publish it next to the
archive it describes. Forecast models archive one whole document per run on
each line. Observation datasets archive one observation object per line
instead, and each instant is written once, the first time it enters the
rolling window.
Publish the dataset root
A build writes only the per-model directories above. The read side also expects four files at the dataset root. A build does not produce any of them, so an upload flow publishes each one as its own step.
| Root file | Produced by |
|---|---|
models.json | meteo forecast catalogue --output emits the packaged model catalogue. Against a bucket, meteo forecast publish --models uploads it in one step. |
sites.json | You author it (Configure launches) and upload it verbatim. It is the one root file the engine does not write. |
site-context.json | meteo forecast terrain --sites ./sites.json --output measures it. Against a bucket, terrain --sync regenerates and publishes it whenever the catalogue has changed. |
runs.json | meteo forecast runs-index --output regenerates it wholesale from the published manifests. publish advances it after every model upload. |
A dataset root without models.json and site-context.json fails the read
side’s discovery and rendering paths, so publish all four before pointing a
consumer at the root.
Treat the manifest and each profile as one publication
pair. Their referenceTime values must agree before rendering; the
@azohra/meteo.briefing/transport loader performs that
check for TypeScript consumers.
Publish with the engine
If your destination is an S3-compatible bucket, meteo forecast publish
can run the upload itself, in the S3 mode that
Environment and credentials describes. This
is one deployment choice among the sync approaches below, and it is
optional.
meteo forecast build --model gfs --sites ./sites.json --output datameteo forecast publish --model gfs --data dataThe verb owns the publication protocol, so an upload script does not have
to restate it. It skips when the build wrote nothing, refuses to publish
a scratch tree older than the published dataset (an unreachable bucket
fails loudly rather than reading as either verdict), uploads history
archives, then site documents, then the manifest, and then regenerates
and uploads runs.json from the published manifests. The manifest is
the publication’s commit point, so nothing it references appears after it.
Every object key comes from the reader contract’s documentPaths.
Cache lifetimes are your deployment’s choice (--cache-live,
--cache-closed-months). The TRIAL defaults suit a 15-minute tick. They
rest on one dataset fact. A closed month archive does not change again,
so your closed-months value may safely be immutable.
After the manifest lands, publish reads it back and parses it with the
reader contract’s guard. A publication that a reader could not consume
fails in the publishing job’s log instead of later in a consumer’s ingest.
Of the dataset-root files, publish
advances runs.json after every model and publish --models uploads the
model catalogue, and terrain --sync keeps the site context in step with
the site catalogue. Only the operator writes sites.json. The engine reads
it (--sites dataset)
and does not write it.
Put the tree on static storage
Copy or sync the output directory only after a successful builder exit.
Keep paths stable, and preserve JSON content types and gzip encoding. Serve
a .jsonl.gz file as application/gzip, or as raw gzip bytes with no
Content-Encoding header, so a reader receives the archive itself and not
a transparently decompressed body. Let caches expire
independently, and do not assume an atomic multi-file deployment. That last
condition is why consumers validate the manifest/profile pair.
Any sync tool that preserves those properties works. To a host you reach over SSH:
rsync -rt --exclude '.*' public/data/ your.host:/var/www/forecasts/data/To an S3-compatible bucket (R2 included), with the archive encoding declared so gzip bytes are served as stored:
aws s3 sync public/data "s3://$METEO_R2_BUCKET/data" \ --endpoint-url "$R2_ENDPOINT" \ --exclude "*.jsonl.gz" --content-type application/jsonaws s3 sync public/data "s3://$METEO_R2_BUCKET/data" \ --endpoint-url "$R2_ENDPOINT" \ --include "*.jsonl.gz" --exclude "*" --content-type application/gzipSync the per-model directories on every build; the dataset-root files change only when you change them.
Possible destinations include a club’s existing static site, object storage, or a private member-gated application. These are deployment choices, and meteo does not provide them. Do not add credentials, launch membership, or access policy to the profile contract.
Validate before rendering
import { readFile } from "node:fs/promises";import { parseSiteForecastJson } from "@azohra/meteo.briefing/contract";import { buildMeteogramScene, renderMeteogramSvg } from "@azohra/meteo.briefing/meteogram";
const source = await readFile("./public/data/hrrr-conus/sites/test-hill.json", "utf8");const profile = parseSiteForecastJson(source);
if (!profile) throw new Error("profile failed contract validation");
const scene = buildMeteogramScene(profile, { timeZone: "America/Vancouver" });const svg = renderMeteogramSvg(scene, { idPrefix: "club-profile" });The package contract, scene, and SVG tests exercise this validation → scene → serialization chain. The input path, timezone, rendering options, palette, hosting destination, and audience remain downstream configuration. An operator can change them without copying formulas or renderer internals.
Downstream access
Authentication, membership, launch discovery, product navigation, alerts, retention, and operational language belong to the downstream application. Choose the access model at the hosting boundary:
| Publication need | Downstream implementation |
|---|---|
| Public club archive | Static host or object storage with explicit retention |
| Member-only current output | Application or edge authorization in front of the static tree |
| Build-time chart pages | Fetch/validate during the downstream build and deploy generated HTML/SVG |
| Custom interactive viewer | Validate documents, build a scene, and own browser state and accessibility |
The JSON contract does not contain users, roles, sessions, launch
permissions, or API keys. Adding them would couple portable forecast
documents to one operator’s policy. A gate that addresses the tree by key
rather than by URL (object storage behind an application, a Cloudflare
Worker reading an R2 bucket binding) builds those keys with
documentPaths,
the same exported layout publish uploads through.
At every access level, keep provider attribution with derived material and state the operator’s operational limitations.