Deploying Documentation

Merging to main is the only way to deploy int-docs.thephenom.app. How the Cloudflare Pages git integration builds production, and why the Docker image is not a promotion path.

Merging a pull request into main deploys production. There is no build step to run, no artifact to upload, and no image to push. Cloudflare Pages watches this repository and rebuilds int-docs.thephenom.app on every push to main.

The one rule

int-docs.thephenom.app is deployed by merging into main. That is the entire promotion path.

If you have merged your PR, you have deployed. If you have not merged, nothing you do locally, in Docker, or on a testing host will reach production.

How production is built

Cloudflare Pages is connected directly to Phenom-earth/phenom-earth-docs through its git integration. No GitHub Actions workflow is involved.

Setting Value
Pages project int-docs-phenom
Cloudflare account 0b61574326148cb1e1a82f00cd160b30
Production branch main
Hugo environment int-docs (see config/int-docs/)
HUGO_VERSION 0.163.0
NODE_VERSION 24

The int-docs environment is deliberately richer than the Hugo default: it includes the internal-only/ content that must appear on the internal docs site. Build settings live in the Cloudflare Pages dashboard, not in this repository, so netlify.toml in the repo root is a leftover from the Docsy upstream template and does not drive the production build.

Sequence

  1. Open a PR against main.
  2. Cloudflare Pages builds a preview deployment for the PR and reports it as the Cloudflare Pages check. A green check means your branch compiles; it does not mean anything has shipped.
  3. Merge the PR.
  4. Cloudflare Pages builds main and deploys it to int-docs.thephenom.app, typically within a few minutes.
  5. Verify by loading the affected page and confirming your content is actually served. Do not treat the deployment status alone as proof.

What is not a deploy path

The Docker image is a personal testing instance, not a promotion path. int-docs.matthewstevens.org is M’s own Hugo container built from Dockerfile.int-docs. It exists so M can preview the site; it is not shared team infrastructure and it never promotes anything to production. Content that lives only in a working tree can appear on that host while being entirely absent from git, which is exactly how unreviewed content reaches a running site unnoticed.

Other developers must run their own testing container. Do not expect access to M’s instance. Build your own from Dockerfile.int-docs and run it locally.

The GitHub Actions deploy workflow is gone, on purpose. deploy-cloudflare-pages.yml was removed in #360 because it pushed a different artifact (the default Hugo environment, without internal-only/) to the same Pages project. With both pipelines live, the two would alternately overwrite each other on every push to main. Do not reintroduce it. mermaid-validate.yml and deploy-mcp-worker.yml are unrelated and stay.

Running the docs locally

Either approach works for previewing your own changes before you open a PR.

# Hugo directly (fast iteration, live reload)
npm install
npm run serve

# Or your own testing container, mirroring the deployed layout
docker build -f Dockerfile.int-docs -t my-phenom-docs .
docker run --rm -p 9666:9666 my-phenom-docs

Gotchas

Hugo and Node versions are coupled, and the coupling has broken production before. Hugo 0.155+ passes Node --permission flags to node-based asset tools, and Hugo 0.162+ calls node:module registerHooks(), which only exists in Node 22.15+ and 24+. Node 20 is a dead zone: too new for Hugo’s legacy path, too old for the modern one. This combination failed a production deploy and was fixed in #329 by moving Cloudflare Pages to Node 24 and disabling security.node.permissions in config/_default/config.yaml.

Five places pin a Hugo version, and they can drift apart. All are currently on 0.163.0; keep them that way.

Location Purpose
HUGO_VERSION in the Cloudflare Pages dashboard production
HUGO_VERSION in Dockerfile.int-docs (builder and runtime stages) personal testing container
HUGO_VERSION in Dockerfile the other container build
hugo-extended in package.json local npm run builds
The version named in CLAUDE.md what agents read as the project’s Hugo

Bump them together and confirm the resulting Hugo runs on Node 24 before merging. A bump that moves only one of the five leaves the lanes building on different Hugo versions, so a green local build stops predicting production.

A green Cloudflare check does not validate a Hugo bump. Cloudflare installs Hugo from its own HUGO_VERSION dashboard variable, so a pull request that raises hugo-extended in package.json gets a passing preview without ever running the bumped binary. Hugo 0.164.0 reached a green check that way while being unbuildable here (template "td-content-after-header" not found). Build the bumped version locally before believing the check.

PostCSS only runs in production builds. Docsy gates the PostCSS and autoprefixer pipeline behind hugo.IsProduction, so verifying a PostCSS or autoprefixer bump requires --environment production. An --environment int-docs build never executes that code path.

Access control

The deployed site is gated by Cloudflare Access.

  1. An Access policy is configured for int-docs.thephenom.app.
  2. It authenticates against the phenom-dev-local Cognito user pool (us-east-1_AkG9mnbjA).
  3. Only users with valid credentials in that pool can view the site.

Because access is gated, treat published content as internal rather than secret. The gate controls who can read the site; it is not a substitute for keeping credentials and unreviewed material out of the repository.