Theming & assets
A space has two visual layers that never collide: the editor chrome — toolbars, drawers, dialogs — and the site content — everything a visitor actually reads. The chrome is fixed; the content is entirely yours.
Fixed chrome, free content
The editor’s own styling is locked. Every space gets the same toolbar, the same drawers, the same monochrome controls, so an editor who learns one CTXR space knows them all. You can’t restyle the chrome, and you wouldn’t want to — consistency there is a feature.
Your site content is the opposite: completely free. Drop any CSS you like into build/assets/css and it styles the public page. Every space starts with a default theme so it looks finished from day one, but that theme is a starting point, not a cage — replace it, extend it, throw it away.
editor chrome → fixed, platform-owned, monochrome (visitors never load it)
site content → build/assets/css, entirely yours (the default theme is a seed)
Visitors never download a byte of editor CSS or JS — those assets load only when an editor is present. The “Powered by CTXR” mark a free space shows, and its removal on a whitelabel plan, are a plan feature, not something you toggle in build/.
Assets without a bundler
There’s no webpack, no Vite, no build step for assets. Two reasons it works:
A shared CDN baseline. Common libraries and the icon set are served once from a shared CDN, cached across every space. You don’t vendor them.
Importmaps with per-file cache-busting. JavaScript ships as native ES modules, wired together by an importmap. Each entry carries a ?v=<mtime> stamp, so changing one file busts only that file’s cache — not the whole bundle.
<script type="importmap">
{ "imports": {
"ctxr-ui-drawer": "/assets/js/ctxr-ui-drawer.js?v=1712666400",
"space-gallery": "/assets/js/space-gallery.js?v=1712670000"
}}
</script>
Skipping the bundler is a deliberate trade. Over HTTP/2, many small files are cheap to serve and individually cacheable — a one-line change ships as a one-file invalidation instead of a fresh megabyte. No build step also means no build step to break.
The module grammar
Your JavaScript follows a naming grammar that mirrors the architecture, so a filename tells you its role and its dependency direction:
| Stem | Layer | Depends on |
|---|---|---|
ctxr-* | platform primitives (the trunk) | nothing below |
space-* | space-specific behaviour (the branch) | ctxr-* |
node-* | per-node features (the leaf) | space-*, ctxr-* |
Imports flow leaf → branch → trunk, never the reverse. A node-* module may import a ctxr-* primitive; a ctxr-* primitive never reaches up to a leaf. Keep the arrows pointing inward and the dependency graph stays acyclic on its own.
CSS is free, JS is gated
The asymmetry is intentional. CSS can’t execute, exfiltrate, or escape — build/assets/css ships with no gate. JavaScript can, so build/assets/js passes the same lint-gate as your templates: no dangerous calls, no external dependencies, no out-of-bounds reach. Style freely; script within the lines.
What’s a client choice, not a platform feature
Two things developers often expect the platform to do, and it deliberately doesn’t:
- Fonts are yours to load — self-host or pull from a font CDN in your CSS. There’s no font manager.
- Image optimisation is a client choice. There’s no native image pipeline resizing or re-encoding uploads; if you want responsive variants, you generate them or front the site with a service that does.
Both omissions are about the cost model: the platform doesn’t run expensive transforms on every upload because someone has to pay for that compute. Keeping image work at the edge keeps the platform cheap to run and the bill predictable.