One deployment, many hosts
Every host below is served by the same Next.js app. middleware.ts
reads the request's Host header and rewrites or redirects before any
route handler runs. The map lives in content/hosts.ts.
| Host | Serves |
|---|---|
steinhoff.group | The holding-company page, rewritten from / → /holding |
neo.steinhoff.group | The personal site — canonical for everything not listed below |
course.steinhoff.group | /courses/*, mounted at its own root |
app.steinhoff.group | The admin CMS, /admin/* mounted at its own root |
bank.steinhoff.group | Steinhoff Private, passphrase-gated, mounted at its root |
protocol.steinhoff.group | The Protocol dossiers, /protocol/* mounted at its root |
potentia.steinhoff.group | Potentia and the canon, /potentia/* mounted at its root |
docs.steinhoff.group | This wiki, /docs/* mounted at its root |
The mount pattern
A subdomain "mounts" a subtree at its own root via NextResponse.rewrite,
not a redirect — the URL the visitor typed stays in the address bar. The
mount() helper in middleware.ts is deliberately idempotent: a path
already inside the mounted tree passes through untouched. That matters
because the admin layout redirects to the absolute path /admin/login, and
a naive prefix would turn that into /admin/admin/login and 404 the login
page on app.steinhoff.group.
Canonical vs. private
Courses, Protocol, and Potentia are public and canonical on their
subdomain — hitting /courses/*, /protocol/*, or /potentia/* on any
other host 308-redirects to the subdomain, so there's exactly one live URL
per page and no duplicate-content problem to argue about.
Docs and Bank are the opposite: private, noindexed on every page, and
disallowed in robots.txt. /docs/* still 308-redirects to
docs.steinhoff.group for the same one-URL reason, but there's no SEO
authority at stake — it's just tidiness.
Shared paths
/_next/* and /api/* are never remounted on any subdomain — rewriting
them would break the analytics beacon and any API route on that host. See
isShared() in middleware.ts.