The two Odal Node websites, odal-node.io and docs.odal-node.io, as one pnpm workspace under the Apache License 2.0. Each site is an Astro project deployed as its own Cloudflare Pages project, and both share one set of brand tokens.
Note
Status: pre-production. The sites describe software that no operator runs
against real product data yet. Every change lands on staging and is reviewed on
its previews before it is promoted to main, which is what the sites serve.
Node (the version in .node-version; package.json states the minimum) and pnpm,
which corepack installs at the version package.json pins.
corepack enable
pnpm install
pnpm dev:landing # http://localhost:4321
pnpm dev:docs # http://localhost:4325; run both and they link to each otherTo check what you changed the way CI will, build first: several checks read the build.
pnpm -r build
pnpm -r check
pnpm run check:links| Folder | Site | Stack | Pages project |
|---|---|---|---|
site/dpp-landing/ |
odal-node.io, www.odal-node.io |
Astro + Tailwind 4 | odal-node-landing |
site/dpp-docs/ |
docs.odal-node.io |
Astro + Starlight, Scalar on /api |
odal-node-docs |
packages/brand-tokens/ |
Colours, type, spacing for both sites |
Each site has its own README for its layout and Pages build settings.
Some pages are generated from data copied out of the other repositories at a pinned commit, so a page cannot describe the software differently from its source:
| What | Pin | Refresh |
|---|---|---|
Product groups and EU acts from dpp-core, with a copy of the acts in site/dpp-docs/src/data/instruments for the docs |
site/dpp-landing/core-source.json |
pnpm --filter dpp-landing run sync:core |
API spec from dpp-engine |
site/dpp-docs/openapi-source.json |
pnpm run sync:openapi, or the nightly workflow |
Never edit the vendored files by hand; bump the pin and sync.
Never type a passport date into a page either. The docs write
<PassportDate act="<id>" />, landing pages call passportDateOf("<id>"), and copy in
site/dpp-landing/src/data writes {passport:<id>}. All three read the vendored acts
and fail the build when an act records no date. Other regulatory dates are written
beside the article that sets them.
CI (.github/workflows/ci.yml) runs all of these on every pull request and every
push to staging or main. check:links, check:spacing, check:llms and
check:sitemaps read the build, so build first.
pnpm -r build
pnpm -r check # types and content collections, not links
pnpm run check:links # internal links and anchors in both builds
pnpm run check:spacing # words glued across a line break
pnpm run check:leakage # internal names and private paths
pnpm run check:llms # llms.txt matches the pages
pnpm run check:sitemaps # sitemaps and their lastmod dates
pnpm run test:scripts
pnpm --filter dpp-landing run check:core # vendored core data matches its pin (DPP_CORE_DIR)
pnpm --filter dpp-landing run test:verify # /verify agrees with the engine's verifier
pnpm run check:openapi # vendored API spec matches its pin (DPP_ENGINE_DIR)
pnpm audit --audit-level high
pnpm run check:external # outside links; weekly in CI, not a PR gatemain is what Cloudflare publishes. Its ruleset matches dpp-core and dpp-engine:
pull request required, squash only, build must pass on an up-to-date branch, no
force-push, no deletion, no bypass.
- Land work on
staging, through a pull request or a direct push once the checks pass. - Review the previews:
staging.odal-node-landing.pages.devandstaging.odal-node-docs.pages.dev. - New or reworked pages get a hand screen-reader and keyboard pass, which
/accessibilitypromises. - Open a pull request from
stagingtomainand squash-merge it. - Reset
stagingtomain, so the next pull request lists only new commits (a squash leaves the old ones onstagingotherwise):git fetch origin && git push --force-with-lease=staging origin origin/main:staging - If pages were added or moved, resubmit both sitemaps in Search Console.
A squash merge sets the sitemap lastmod of every page it touches to the merge date.
Dates come from git history (scripts/git-lastmod.mjs).
| Workflow | When | What |
|---|---|---|
ci.yml |
Pull requests, pushes to staging and main |
The checks above |
advisories.yml |
Daily, 06:20 UTC | pnpm audit --audit-level high against the committed lockfile, so a new advisory surfaces on a day nothing was committed |
sync-openapi.yml |
Nightly, 03:17 UTC | Opens a pull request into staging when the engine's spec changes |
external-links.yml |
Mondays, 04:41 UTC | check:external |
| Dependabot | Mondays | Version updates against staging, grouped: the Astro packages together, other npm minor and patch updates together, and all GitHub Actions together. TypeScript 7.0 is held back because astro check refuses it. Security updates always target main; retarget those to staging |
Scheduled workflows run from main only. Every workflow that sets up Node reads its
version from .node-version.
- Pages: each project builds from the repository root (see the site READMEs). Production is
main. Every branch gets a preview at<branch>.<project>.pages.dev, which Cloudflare marksnoindex. - Headers: each site's
public/_headerssets HSTS, a same-origin CSP and caching. no-transformon HTML: the free plan injects a bot-detection script into every HTML page, and it cannot be switched off. The script sets acf_clearancecookie, which the privacy page says the sites do not set.Cache-Control: no-transformon HTML stops that injection and Cloudflare's email obfuscation, at the cost of Cloudflare no longer compressing HTML. Other files stay compressed.- Editing cache rules: Pages joins two values of one header with a comma. A rule that sets its own
Cache-Controlmust first detach the inherited one with! Cache-Control, and no two cache rules may match the same file. - Zone: the
/verifyrate-limit rule (20 requests per 10 s per IP) is kept as code inscripts/cloudflare-rate-limit.mjs(pnpm run rate-limit:verify, add-- --applyto write; the token needs "Zone WAF: Edit"). It does not coverpages.devpreviews. - Cache purge: run
.github/workflows/deploy.ymlby hand.
Both sites publish sitemap-index.xml, robots.txt (content signals:
search=yes, ai-input=yes, ai-train=no) and llms.txt. On the landing,
src/lib/site-map.ts is the one list of pages behind the nav, footer, /sitemap and
llms.txt; on the docs, src/sidebar.mjs is. Submit both sitemaps in Search Console.
No page loads anything from another origin (the CSP allows only the site itself), and
there is no analytics. The API reference sends "Test request" straight to the reader's
node (proxyUrl: '' in site/dpp-docs/src/scripts/mount-api-reference.ts); check
that this still holds after upgrading @scalar/api-reference.
The product's source is in dpp-core
(Apache-2.0) and dpp-engine (BSL-1.1).
This repository holds only the websites.
Apache License 2.0 for the sites' source. It grants no use of the Odal Node name or logo.
Report vulnerabilities privately to security@odal-node.io, never in a public issue. See the organization's SECURITY.md.