The two Odal Node websites, as one pnpm workspace deployed as two Cloudflare Pages projects.
| 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.
Node 22.13+ and pnpm (version pinned in package.json, via corepack).
pnpm install
pnpm dev:landing # http://localhost:4321
pnpm dev:docs # http://localhost:4325; run both and they link to each otherCI (.github/workflows/ci.yml) runs all of these on every pull request and every push to main. Pushes to staging run no CI, so run them locally first. 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 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 gate| What | Pin | Refresh |
|---|---|---|
Product groups and EU acts from dpp-core |
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.
main 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).
- 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.
| Workflow | When | What |
|---|---|---|
ci.yml |
Pull requests, pushes to main |
The checks above |
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 | Weekly | npm and Actions updates. Its pull requests target main; retarget them to staging |
Scheduled workflows run from main only.
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-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.