The marketing site for the Klassd suite — Klassd CMS
(/, /docs), Klassd.Workflows (/workflows),
and Klassd.Auth (/auth) — built the way a headless CMS
is meant to be used: the CMS runs on its own and exposes a JSON delivery API, and a separate
Vue 3 SSR frontend consumes it.
website/
├─ klassd/ # Klassd CMS, as a git submodule (dogfooded against source pre-1.0)
└─ src/
├─ Backend/ # ASP.NET host wiring Klassd — serves /admin + the public /api (.NET 10)
└─ Frontend/ # Vue 3 server-side-rendered site that reads /api (Bun + Vite)
The Backend renders no public HTML — it's purely the CMS (admin UI + headless API). The Frontend
fetches content from /api/pages during server-side rendering, returns fully-rendered HTML, then
hydrates on the client.
- .NET 10 SDK
- Bun (≥ 1.3)
- Git (the repo uses a submodule)
Clone with submodules (or run git submodule update --init after cloning):
git clone --recurse-submodules git@github.com:getklassd/website.git
cd website1. Run the CMS (src/Backend) on http://localhost:5080:
dotnet run --project src/BackendOn first run it creates a SQLite database, seeds an admin/admin account, and seeds a starter
HomePage so the frontend has content. The admin is at http://localhost:5080/admin; the delivery
API is at http://localhost:5080/api/pages.
2. Run the site (src/Frontend) on http://localhost:5173:
cd src/Frontend
bun install
bun run devOpen http://localhost:5173. Edit the HomePage in the CMS admin and refresh — the SSR'd page reflects it (content comes from the CMS, not the code).
- Content model lives in
src/Backend/Content/as C# classes (HomePage,FeatureBlock). Klassd reflects over them to build the admin editor. - Delivery: the frontend calls the anonymous
GET /api/pages?locale=en, finds theHomePage, and renders itsdatafields +featuresblock area. Types mirror the payload insrc/Frontend/src/api.ts. - SSR data flow:
entry-server.tsfetches on the server →renderToString→ HTML + embeddedwindow.__INITIAL_STATE__→entry-client.tshydrates with that state (no client refetch).
cd src/Frontend
bun run build # type-check + client bundle (dist/client) + SSR bundle (dist/server)
NODE_ENV=production bun run startConfigure where the SSR server reaches the CMS with CMS_BASE (runtime) or VITE_CMS_BASE
(build-time); see src/Frontend/.env.example. When the site and CMS are on different origins, set
the site origin in the CMS's CORS allow-list (Klassd:Cors:AllowedOrigins in
src/Backend/appsettings.json).
Both services are containerized; docker compose runs the stack with the site talking to the CMS
over the internal network:
docker compose up --build
# site → http://localhost:5173
# admin → http://localhost:5080/admin (seeded admin/admin — change it)src/Backend/Dockerfile— CMS image. Build context is the repo root (it needs theklassd/submodule):docker build -f src/Backend/Dockerfile .. The SQLite DB persists in thecms-datavolume (/data).src/Frontend/Dockerfile— Vue SSR image (docker build src/Frontend)..github/workflows/ci.ymlbuilds both on every push/PR (checks out the submodule over HTTPS).
The site fetches the CMS server-side (
CMS_BASE), so the CMS port doesn't need to be public. Media URLs (/api/media/...) are the exception — if you use uploaded media, the CMS must be reachable from the browser and its origin added to the CORS allow-list. The seeded content uses no media (the logo falls back to the "Klassd" wordmark).
The site can also be shipped as a fully static bundle — no Bun server, no CMS at
runtime. src/Frontend/prerender.ts runs the same render() the SSR server uses, fetches
the content from the CMS once at build time, and bakes the HTML + hydration state into
dist/client. The result is self-contained (the client hydrates from inlined state and never
calls the CMS).
# with the CMS running on :5080
cd src/Frontend
CMS_BASE=http://localhost:5080 bun run build:static # build + prerender → dist/client.github/workflows/pages.yml does this in CI: it builds and boots the CMS, prerenders against
it, and deploys dist/client to GitHub Pages. It serves at the custom domain getklassd.com
(base /, with public/CNAME); set BASE_PATH=/website/ to serve from a project-page URL
instead. One-time setup: in the repo's Settings → Pages, set the source to GitHub Actions,
and point the domain's DNS at GitHub Pages.
- Pre-1.0, the Backend references Klassd via the
klassd/submodule + project references so the site builds against live CMS source. Klassd is now published to NuGet (prerelease) — you can swap the project references for<PackageReference Include="Klassd.*" Version="..." />insrc/Backend/GetKlassd.Cms.csprojif you'd rather build against the packages. - Bun is the JS runtime and package manager. The SSR server (
src/Frontend/server.ts) usesnode:http(which Bun implements) to bridge Vite's dev middleware.