Customize Client Styling
GoatDB builds one stylesheet at /index.css. Use
cssPath for base styles
and import component or generated styles from the
jsPath client entry
point. These AppConfig options apply to
both compile() and
startDebugServer().
Start with CSS files
The generated project already configures
cssPath as
./client/index.css. Keep shared tokens and global rules there, then import
styles close to the component that needs them:
:root {
color-scheme: dark;
--accent: #c6f578;
}
import './app.css';
.app {
color: var(--accent);
}
GoatDB concatenates CSS into /index.css in this order:
cssPathbase stylesheet.- CSS imported by
jsPath, including transitive imports. - CSS from remaining entry chunks, in entry order.
Later rules therefore override earlier rules at equal CSS specificity. Import a component stylesheet from that component when its rules should override the base stylesheet.
Add generated CSS with a plugin
Pass an EsbuildPlugin through
esbuildPlugins when styles come from a
generator or a non-CSS source. This example supplies a
virtual theme stylesheet; replace its contents with your generator output.
import {
compile,
type EsbuildPlugin,
} from '@goatdb/goatdb/server/build';
const themePlugin: EsbuildPlugin = {
name: 'theme',
setup(build) {
build.onResolve({ filter: /^virtual:theme$/ }, () => ({
path: 'theme',
namespace: 'goatdb-theme',
}));
build.onLoad({ filter: /.*/, namespace: 'goatdb-theme' }, () => ({
contents: ':root { --accent: #c6f578; }',
loader: 'css',
}));
},
};
await compile({
serverEntry: './server/server.ts',
jsPath: './client/index.tsx',
htmlPath: './client/index.html',
cssPath: './client/index.css',
buildDir: './build',
esbuildPlugins: [themePlugin],
});
Import the virtual stylesheet from the client entry point so esbuild includes
it in /index.css:
import 'virtual:theme';
GoatDB registers adapter stubs first, then user plugins, its asset fallback,
and finally the Deno resolver/loader. A plugin can therefore resolve an
original specifier and load its CSS before GoatDB's fallbacks handle it. Pass
the same
esbuildPlugins array to
startDebugServer()
to use the identical pipeline during development.
Bundle images and fonts referenced by CSS
Use local relative paths in CSS. Esbuild emits each referenced file under
/assets/ with a content-hashed name and rewrites the final CSS URL.
.logo {
background-image: url('./logo.svg');
}
:::caution Behavior change
cssPath is now processed by esbuild (previously it was copied verbatim into
/index.css). Relative url() targets must exist on disk at build time — they
are bundled and content-hashed, so references to files served from other routes
(e.g. assetsPath) no longer resolve.
:::
Do not point url() at a file made available through
assetsPath. Those
files are served separately, while CSS url() inputs must exist locally for
esbuild to bundle them. A missing CSS asset fails the build instead of becoming
a browser 404.
Debug and inspect output
compile() minifies CSS by
default; pass minify as
false when readable output is required.
startDebugServer()
rebuilds the same CSS pipeline without production minification. CSS source maps
are served at /index.css.map: a single CSS chunk keeps its own flat map with
the URL rewritten to index.css.map. When multiple chunks are concatenated,
the map is an indexed source map (Source Map v3 sections format): each chunk's
map is embedded verbatim and positioned at its start line in the concatenation,
so rules from every chunk resolve to their authored sources.
The debug server watches only
watchDir
(the current directory by default). watchFiles and watchDirs returned by an
esbuild plugin do not expand that watcher. Keep generator inputs beneath
watchDir,
or set it to their common parent.
Verify a styling change
- Run
deno task devornpm run devand inspect/index.cssin the browser. - Confirm a CSS
url()was rewritten to a hashed/assets/URL. - In browser devtools, confirm
/index.css.mapmaps a rule to its authored stylesheet. - Run the production build to catch missing CSS assets and minification-only issues.