neuf-www
The neuf.no website. Wagtail CMS backend (dnscms/) feeding a Next.js frontend (web/) over GraphQL.
Tools are managed by mise. Run mise install to get python, uv, node, and prek.
Backend (dnscms/)
cd dnscms
uv sync
uv run ./manage.py migrate
uv run ./manage.py runserver
uv run pytest
GraphQL endpoint: http://127.0.0.1:8000/api/graphql/.
Frontend (web/)
cd web
npm install
npm run dev # http://localhost:3000
npm run codegen # regenerate GraphQL types (needs the backend running)
npm run build
npm test # vitest (npm run test:watch for watch mode)
Pre-commit hooks
prek runs ruff lint + format on dnscms/**/*.py, vitest on web/ changes, plus a few sanity hooks. Hooks are configured in prek.toml.
prek install # registers the git hook
prek run --all-files # run on everything
Caching and revalidation
The frontend caches all pages and GraphQL responses (ISR) under a single cms cache tag.
Wagtail purges it via a webhook on any publish/unpublish/move/delete (see dnscms/dnscms/revalidation.py).
Date-sensitive queries (futureEvents, opening hours) additionally expire at Oslo midnight.
REVALIDATE_WEBHOOK_SECRET must be set to the same value on both sides
(web/.env.local and dnscms/dnscms/settings/local.py or env); unset disables the webhook.
# manual purge
curl -X POST -H "X-Revalidate-Secret: $SECRET" https://<frontend>/api/revalidate
Note: ISR is off under npm run dev; use npm run build && npm run start to test caching.
Production proxy (Caddy)
Both apps run on Coolify on ds.neuf.no behind the coolify-proxy container
(caddy-docker-proxy). Extra config lives in /data/coolify/proxy/caddy/dynamic/ on the host,
not in any repo:
-
Caddyfile,default_redirect_503.caddy: Coolify's own. -
studentfestivalen-redirects.caddy:studentfestivalen.noandstudioweb.no→neuf.no/studio. -
cms-neuf-no.subdirectives:rewrite / /admin/ handle_path /media/* { root * /site/cms.neuf.no/media file_server } -
neuf-no.subdirectives. The header replaces Next'sstale-while-revalidate, which made browsers reuse pages from old builds and broke navigation after deploys:handle_path /media/* { root * /site/cms.neuf.no/media file_server } @pages not path /_next/static/* /_next/image* /assets/* /media/* header @pages >Cache-Control "public, max-age=0, must-revalidate"
CMS login via Galtinn
Editors log in to the CMS with their Galtinn account over OIDC (see dnscms/dnscms/oidc.py).
Access follows membership of one Galtinn group, which is checked at every login:
members become Wagtail editors, and a linked user who has left the group is deactivated.
Users are identified only by their stored Galtinn ID, never by username or email,
so an existing local user must be linked by hand under /django-admin/ before they can use it.
Linked users have no local password; unlinked local users still log in with theirs.
Set these in dnscms/dnscms/settings/local.py or env. The Galtinn button only shows when the group and client ID are set.
| Setting | Value |
|---|---|
GALTINN_BASE_URL |
Galtinn's public URL, defaults to https://galtinn.neuf.no |
GALTINN_EDITOR_GROUP |
name of the Galtinn group that grants editor access |
OIDC_RP_CLIENT_ID, OIDC_RP_CLIENT_SECRET |
the OAuth application registered in Galtinn's admin |
WAGTAIL_BASE_URL |
must be the CMS's public URL; the callback is <WAGTAIL_BASE_URL>/oidc/callback/ |
To link existing local users in bulk, match them against Galtinn and review the result by hand:
uv run manage.py galtinn_users export > cms-users.csv # in dnscms
uv run manage.py match_cms_users cms-users.csv > matched.csv # in galtinn, suggests a galtinn_id per row
uv run manage.py galtinn_users link matched.csv # in dnscms, dry run; add --apply to save
Rows with an empty galtinn_id stay local users. A linked user keeps their password until their first Galtinn login.