CSS pipelines
Flexweg CMS supports two CSS workflows for themes — picked per-theme based on which files exist:
- SCSS pipeline:
theme.scssexists, notailwind.config.cjs. Vite compiles SCSS at build time. Used by the default theme. - Tailwind pipeline:
theme.css+tailwind.config.cjsboth exist. A separate prebuild step runs the Tailwind CLI before Vite. Used by magazine and corporate.
Only one is active per theme. You can mix both across themes in the same admin (the build script auto-detects).
SCSS pipeline
When to use
- You're comfortable with SCSS (variables, nesting, mixins).
- You want fine control over the generated CSS without a utility-class layer.
- Your theme is mostly hand-written CSS with maybe a few semantic primitives.
Setup
src/themes/<id>/
├── manifest.ts
└── theme.scss ← your styles
In manifest.ts:
import cssText from "./theme.scss?inline";
export const manifest: ThemeManifest = {
id: "my-theme",
scssEntry: "theme.scss",
cssText,
// …
};
Vite's ?inline suffix means the SCSS gets compiled and the resulting CSS is exposed as a string on cssText. The build script's scripts/build-themes.mjs takes the same compiled output and writes it to dist/theme-assets/<id>.css.
What your theme.scss needs
A typical theme.scss starts with CSS variables (for compileCss to override) and font imports:
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap");
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-primary: #3b82f6;
--font-sans: "Inter", sans-serif;
// …
}
body {
font-family: var(--font-sans);
background: var(--color-bg);
color: var(--color-text);
}
// … rest of your styles
The variables in :root are what your compileCss(config) hook will override at runtime when the user customises the theme.
Iterative dev
npm run dev watches theme.scss via Vite HMR — edits show up instantly in the editor preview without a reload.
Tailwind pipeline
When to use
- You want to author with utility classes.
- You like Tailwind's design tokens (colours, spacing, typography).
- You're building component-heavy UIs where utility classes win on velocity.
Setup
src/themes/<id>/
├── manifest.ts
├── theme.css ← your input file (with @tailwind directives)
├── theme.compiled.css ← generated by the prebuild step (gitignored)
└── tailwind.config.cjs ← scoped to this theme
theme.css includes the standard Tailwind directives + any custom layers:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn-primary {
@apply px-4 py-2 bg-primary text-on-primary rounded;
}
}
tailwind.config.cjs MUST scope content to your theme directory only — otherwise admin-bundle utility classes leak in:
module.exports = {
content: [
"./src/themes/my-theme/**/*.{ts,tsx,html,js}",
],
theme: {
extend: {
colors: {
bg: "rgb(var(--color-bg) / <alpha-value>)",
primary: "rgb(var(--color-primary) / <alpha-value>)",
// …
},
},
},
};
In manifest.ts:
import cssText from "./theme.compiled.css?inline";
export const manifest: ThemeManifest = {
id: "my-theme",
scssEntry: "theme.css",
cssText,
// …
};
You import ?inline from the compiled file, not the source.
How the build wires up
scripts/build-theme-tailwind.mjs runs before Vite (wired into prebuild and predev in package.json). For each theme with a tailwind.config.cjs:
- Invoke the Tailwind CLI:
tailwindcss -c tailwind.config.cjs -i theme.css -o theme.compiled.css - Vite then sees
theme.compiled.cssand compiles it intocssText - The post-build script copies the compiled CSS to
dist/theme-assets/<id>.css
So the order is: prebuild (Tailwind) → vite build → post-build copy.
Iterative dev
Run the Tailwind watcher in a separate terminal:
npx tailwindcss -c src/themes/my-theme/tailwind.config.cjs \
-i src/themes/my-theme/theme.css \
-o src/themes/my-theme/theme.compiled.css \
--watch
Vite HMR picks up the rewritten file. Without the watcher, you'd need to manually run the prebuild every time you change a class.
.js files in content glob
If your theme ships runtime loaders (menu-loader.js, posts-loader.js) whose DOM has Tailwind class names, include .js in the content glob:
content: ["./src/themes/my-theme/**/*.{ts,tsx,html,js}"],
Without this, Tailwind's purge step strips the matching utility classes from your CSS, and the runtime-injected DOM renders unstyled. The magazine theme's burger menu, related-posts widget, etc. all rely on this — magazine's tailwind.config.cjs includes .js in the glob.
If it's not feasible to scan a particular file, use Tailwind's safelist for one-off classes — but it doesn't scale to a whole loader's DOM.
Comparing approaches
| SCSS | Tailwind | |
|---|---|---|
| Setup complexity | Low | Medium (extra prebuild step) |
| Build time | Fast | Slower (Tailwind CLI runs first) |
| Authoring style | Hand-written CSS, BEM | Utility classes |
| Refactoring | Hard (find all usages) | Easy (rename a class, gone) |
| File size | Smaller (no unused classes shipped) | Tiny if purge is configured (otherwise huge) |
compileCss integration | Append :root { … } block | Same — Tailwind respects later :root declarations |
| Runtime customisation | Easy | Easy |
Both approaches need compileCss
Whichever pipeline you use, if your theme has a settings page that exposes user-customisable design tokens, you need a compileCss(config) hook. Without it, the Sync theme assets button uploads the build-time cssText and erases customisations.
The hook works the same for both pipelines:
- Take the bundled
cssText(your full compiled CSS). - Make string-level edits — swap font URLs in
@importlines, append a:root { … }block at the end. - Return the new CSS.
See src/themes/default/style.ts (SCSS) and src/themes/magazine/style.ts (Tailwind) for reference implementations.