@freva-org/portal-builder
v2612.0.1
Published
FP-001 build-time portal framework: closed YAML in, one immutable static artifact out. Astro compiles the pages; nothing about the site is fetched at runtime.
Readme
@freva-org/portal-builder
Build a Freva portal from declarative inputs. Closed YAML in, one immutable static artifact out.
Everything about the site — its routes, navigation, theme, landing blocks and prose — is decided while the artifact is built. The generated browser application still calls live APIs for live things (data search, STAC, authentication), but it never asks a service what the site is. That single rule is why a portal built this way keeps working when the settings service is down, why a deep link is a real file, and why two builds of the same inputs produce the same bytes.
This is the build-time line. The existing
@freva-org/portalpackage publishes a prebuilt runtime site and remains a maintenance line during migration; the two coexist deliberately and do not share an executable name.
Install
The supported CI path is the canonical image, pinned by digest:
docker run --rm --network=none \
-v "$PWD:/src:ro" -v "$PWD/build:/out" \
ghcr.io/freva-org/portal-builder@sha256:<digest> \
build --source-root /src --config /src/portal/portal.yaml --out /out/portalFor local development against Node:
npm install --save-dev @freva-org/portal-builder
npx freva-portal-builder validate --source-root . --config portal/portal.yamlThe npm path installs neither Python nor a browser; the image provides both.
reStructuredText needs the pinned
freva-portal-rsthelper with its exact Docutils. The builder checks the whole handshake and stops (FP1701) if it does not match.Mermaid diagrams are drawn at build time in Chromium, driven by Playwright. Playwright is an optional peer dependency and its browsers are a separate download, so a site with diagrams needs both:
npm install --save-dev playwright npx playwright install --with-deps chromium # --with-deps: Chromium's system libraries (Linux, root)Or set
FREVA_PORTAL_CHROMIUMto an existing Chromium executable. Without a browser the build stops withFP1702, and its hint names the command for the cause it hit (no Playwright, no downloaded browser, or missing system libraries). A site with no diagram never starts a browser.STAC Browser materials, for a portal that enables the component, are prepared by
freva-portal-builder prepare-stac --out <dir>(below). It needsgit,npmand the network;buildnever does.
The commands
freva-portal-builder validate --source-root . --config portal/portal.yaml
freva-portal-builder build --source-root . --config portal/portal.yaml --out build/portal
freva-portal-builder dev --config portal/portal.yaml
freva-portal-builder preview --dir build/portal
freva-portal-builder verify --dir build/portal
freva-portal-builder host-check --dir build/portal --url https://portal.example.org/
freva-portal-builder migrate --from ui-manifest.json --out portal/
freva-portal-builder prepare-stac --out .stac-materials # only if stac-browser is enabled
freva-portal-builder prepare-playground --source-root . --config portal/portal.yaml --out .python-materials
freva-portal-builder prepare-notebook --source-root . --config portal/portal.yaml --out .notebook # only with pythonPlayground.notebook--source-root is the trust anchor and is mandatory for validate and build:
every config, landing, prose, asset, download and subsite input must resolve
inside it after normalization and after the filesystem has canonicalized it.
Symlinks are refused. The output tree must be outside every declared input root,
and that is checked before anything is created.
There is no --runtime, no --api-settings and no silent HTTP fallback.
A minimal site
site/
├── portal.yaml
├── landings/home.yaml
├── content/guide.md
└── assets/{logo.svg,favicon.svg}schemaVersion: 1
site:
id: example-research
title: Example Research Portal
language: en
# The exact absolute site-base URL. Its pathname is the one and only base path.
canonicalUrl: https://portal.example.org/
identity:
logo: ./assets/logo.svg
favicon: ./assets/favicon.svg
theme:
preset: default
rendering:
profile: portal-content-v1
sources:
- root: ./content
mount: /docs/
landings:
home:
path: /
source: ./landings/home.yamlexamples/minimal-portal and examples/full-portal in this repository are
working fictional consumers; the full one exercises every part of the contract.
What the configuration can and cannot say
| You describe | You do not describe | | ------------------------------------------------ | -------------------------------- | | Site identity, chrome, a registered theme preset | CSS, fonts, or component styling | | Named landings composed from framework blocks | Astro components or JavaScript | | Content roots of Markdown and RST | Renderer plugins | | Public service URLs by kind | Credentials of any kind | | Which components are compiled in | How they are implemented | | Trusted documentation subsites, explicitly | Arbitrary active files |
Every portal-owned object is closed: an unknown key is an error with a JSON Pointer and a line, never a quietly ignored extension point.
Components
Three built-in components, each enabled by one field:
| Kind | Service kind | Route | What "disabled" means |
| -------------- | ------------- | ---------------------------- | ------------------------------------------------------------------ |
| databrowser | databrowser | required when enabled | no route, no navigation entry, no module in the bundle, no request |
| stac-browser | stac | required when enabled | additionally no copied application files |
| auth | auth | one technical callback route | no callback page, no account control, no auth client |
Absence is proved from the recorded build graph and copy manifest in
component-evidence.json, not inferred from filenames. Hiding a component with
CSS satisfies nothing.
The artifact
build/portal/
├── index.html # and one real HTML file per route
├── 404.html
├── _portal/ # content-hashed framework assets
├── identity/ # sanitized logo and favicon
├── portal-manifest.json # routes, components, services, files, cache classes
├── input-manifest.json # every input hashed, plus builder/schema/profile identity
├── component-evidence.json # what the bundler and the copier actually did
├── host-policy.json # what a conforming host must do
├── BUILDINFO.json
└── checksums.sha256The output directory is the document root, mounted at the pathname of
site.canonicalUrl. For https://example.org/portal/, index.html is served as
/portal/; the builder does not emit dist/portal/index.html, and serving the
same directory at / is a deployment error rather than a supported relocation.
Documentation
- Consumer guide — repository shapes, CI, deployment
- Configuration reference — every field, with its rules
- Customisation — layout options,
portal-style-v1,portal-template-v1 portal-content-v1— the accepted Markdown and RST- Components and services — enablement, options, evidence
- Hosting — the conformance contract and recipes
- Migrating from the runtime portal
- Developing the builder
Licence
BSD-3-Clause.
