Skip to content

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.

Static artifacts cross explicit boundaries.Generation owns model-dependent quantities; the package validates and renders the published profile document.

Responsibility sequence

  1. The forecast engine generates model-dependent values. Quantities that require inputs beyond the published document stay in the engine.
  2. Consumers validate at the document boundary. They accept only profile documents that satisfy the exported contract.
  3. The operator publishes static artifacts, putting versioned JSON and generated assets on infrastructure the operator controls.
  4. 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.json

Each 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 fileProduced by
models.jsonmeteo forecast catalogue --output emits the packaged model catalogue. Against a bucket, meteo forecast publish --models uploads it in one step.
sites.jsonYou author it (Configure launches) and upload it verbatim. It is the one root file the engine does not write.
site-context.jsonmeteo forecast terrain --sites ./sites.json --output measures it. Against a bucket, terrain --sync regenerates and publishes it whenever the catalogue has changed.
runs.jsonmeteo 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.

Terminal
meteo forecast build --model gfs --sites ./sites.json --output data
meteo forecast publish --model gfs --data data

The 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.

The publication protocol A sequence diagram across four actors — the upstream provider, the forecast job, the static dataset, and the reader's browser. The build phase resolves and fetches one run; the publish phase uploads history, site documents, the manifest as the commit point, and runs.json in that order, with a read-back check; the read phase shows the browser fetching manifest and profile in a retry loop that treats a disagreeing pair as stale.

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:

Terminal
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:

Terminal
aws s3 sync public/data "s3://$METEO_R2_BUCKET/data" \
--endpoint-url "$R2_ENDPOINT" \
--exclude "*.jsonl.gz" --content-type application/json
aws s3 sync public/data "s3://$METEO_R2_BUCKET/data" \
--endpoint-url "$R2_ENDPOINT" \
--include "*.jsonl.gz" --exclude "*" --content-type application/gzip

Sync 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

render-profile.ts
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 needDownstream implementation
Public club archiveStatic host or object storage with explicit retention
Member-only current outputApplication or edge authorization in front of the static tree
Build-time chart pagesFetch/validate during the downstream build and deploy generated HTML/SVG
Custom interactive viewerValidate 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.